aboutsummaryrefslogtreecommitdiffstats
path: root/lisp
diff options
context:
space:
mode:
authorHelmut Eller2026-02-13 09:10:16 +0100
committerHelmut Eller2026-02-13 09:10:16 +0100
commit91c9e9883488d715a30877dfd7641ef4b3c62658 (patch)
treee2c4525147e443f86baf9d0144aeadec082d7564 /lisp
parent9a4a54af9192a6653164364c75721ee814ffb1e8 (diff)
parentf1fe4d46190263e164ccd1e066095d46a156297f (diff)
downloademacs-feature/igc.tar.gz
emacs-feature/igc.zip
Merge branch 'master' into feature/igcfeature/igc
Diffstat (limited to 'lisp')
-rw-r--r--lisp/align.el42
-rw-r--r--lisp/battery.el55
-rw-r--r--lisp/calendar/appt.el30
-rw-r--r--lisp/calendar/cal-bahai.el3
-rw-r--r--lisp/calendar/cal-china.el33
-rw-r--r--lisp/calendar/cal-coptic.el3
-rw-r--r--lisp/calendar/cal-dst.el49
-rw-r--r--lisp/calendar/cal-french.el2
-rw-r--r--lisp/calendar/cal-hebrew.el7
-rw-r--r--lisp/calendar/cal-html.el20
-rw-r--r--lisp/calendar/cal-islam.el2
-rw-r--r--lisp/calendar/cal-julian.el2
-rw-r--r--lisp/calendar/cal-mayan.el6
-rw-r--r--lisp/calendar/cal-menu.el2
-rw-r--r--lisp/calendar/cal-move.el6
-rw-r--r--lisp/calendar/cal-persia.el3
-rw-r--r--lisp/calendar/cal-tex.el47
-rw-r--r--lisp/calendar/calendar.el338
-rw-r--r--lisp/calendar/diary-icalendar.el3730
-rw-r--r--lisp/calendar/diary-lib.el102
-rw-r--r--lisp/calendar/icalendar-ast.el957
-rw-r--r--lisp/calendar/icalendar-macs.el1134
-rw-r--r--lisp/calendar/icalendar-mode.el611
-rw-r--r--lisp/calendar/icalendar-parser.el4887
-rw-r--r--lisp/calendar/icalendar-recur.el2148
-rw-r--r--lisp/calendar/icalendar-utils.el754
-rw-r--r--lisp/calendar/icalendar.el625
-rw-r--r--lisp/calendar/lunar.el2
-rw-r--r--lisp/calendar/solar.el4
-rw-r--r--lisp/calendar/timeclock.el6
-rw-r--r--lisp/cedet/semantic/decorate/include.el32
-rw-r--r--lisp/cedet/semantic/util.el7
-rw-r--r--lisp/comint.el3
-rw-r--r--lisp/desktop.el10
-rw-r--r--lisp/doc-view.el2
-rw-r--r--lisp/emacs-lisp/byte-opt.el13
-rw-r--r--lisp/emacs-lisp/bytecomp.el6
-rw-r--r--lisp/emacs-lisp/checkdoc.el29
-rw-r--r--lisp/emacs-lisp/cond-star.el24
-rw-r--r--lisp/emacs-lisp/crm.el19
-rw-r--r--lisp/emacs-lisp/loaddefs-gen.el7
-rw-r--r--lisp/emacs-lisp/package-activate.el145
-rw-r--r--lisp/emacs-lisp/package.el116
-rw-r--r--lisp/emacs-lisp/seq.el4
-rw-r--r--lisp/emacs-lisp/shortdoc.el4
-rw-r--r--lisp/emacs-lisp/smie.el2
-rw-r--r--lisp/emacs-lisp/subr-x.el104
-rw-r--r--lisp/frameset.el15
-rw-r--r--lisp/gnus/gnus-group.el1
-rw-r--r--lisp/gnus/gnus-topic.el28
-rw-r--r--lisp/help-fns.el19
-rw-r--r--lisp/help-mode.el10
-rw-r--r--lisp/icomplete.el34
-rw-r--r--lisp/isearch.el4
-rw-r--r--lisp/json.el11
-rw-r--r--lisp/language/indian.el9
-rw-r--r--lisp/leim/quail/ipa.el1
-rw-r--r--lisp/leim/quail/iroquoian.el198
-rw-r--r--lisp/net/dbus.el12
-rw-r--r--lisp/net/newst-backend.el13
-rw-r--r--lisp/net/shr.el2
-rw-r--r--lisp/net/tramp-adb.el23
-rw-r--r--lisp/net/tramp-archive.el2
-rw-r--r--lisp/net/tramp-crypt.el4
-rw-r--r--lisp/net/tramp-gvfs.el10
-rw-r--r--lisp/net/tramp-rclone.el94
-rw-r--r--lisp/net/tramp-sh.el53
-rw-r--r--lisp/net/tramp-smb.el20
-rw-r--r--lisp/net/tramp-sshfs.el2
-rw-r--r--lisp/net/tramp-sudoedit.el11
-rw-r--r--lisp/net/tramp.el19
-rw-r--r--lisp/pixel-scroll.el17
-rw-r--r--lisp/progmodes/eglot.el65
-rw-r--r--lisp/progmodes/elisp-mode.el4
-rw-r--r--lisp/progmodes/etags-regen.el2
-rw-r--r--lisp/progmodes/etags.el10
-rw-r--r--lisp/progmodes/make-mode.el18
-rw-r--r--lisp/progmodes/project.el147
-rw-r--r--lisp/progmodes/python.el40
-rw-r--r--lisp/progmodes/xref.el23
-rw-r--r--lisp/replace.el3
-rw-r--r--lisp/subr.el12
-rw-r--r--lisp/system-sleep.el513
-rw-r--r--lisp/textmodes/yaml-ts-mode.el12
-rw-r--r--lisp/time.el16
-rw-r--r--lisp/tutorial.el274
-rw-r--r--lisp/vc/diff-mode.el170
-rw-r--r--lisp/vc/vc-dispatcher.el6
-rw-r--r--lisp/vc/vc-git.el143
-rw-r--r--lisp/vc/vc.el69
-rw-r--r--lisp/wid-edit.el22
-rw-r--r--lisp/window.el2
92 files changed, 17141 insertions, 1159 deletions
diff --git a/lisp/align.el b/lisp/align.el
index 1f1c8f58009..362d59f2231 100644
--- a/lisp/align.el
+++ b/lisp/align.el
@@ -1407,11 +1407,18 @@ aligner would have dealt with are."
1407 (align-region 1407 (align-region
1408 beg end 'entire 1408 beg end 'entire
1409 exclude-rules nil 1409 exclude-rules nil
1410 ;; Use markers for exclusion area bounds so
1411 ;; they remain accurate after subsequent
1412 ;; alignment sections modify the buffer.
1410 (lambda (b e mode) 1413 (lambda (b e mode)
1411 (or (and mode (listp mode)) 1414 (or (and mode (listp mode))
1415 (let ((bm (copy-marker b))
1416 (em (copy-marker e t)))
1417 (push bm markers)
1418 (push em markers)
1412 (setq exclude-areas 1419 (setq exclude-areas
1413 (cons (cons b e) 1420 (cons (cons bm em)
1414 exclude-areas))))) 1421 exclude-areas))))))
1415 (setq exclude-areas 1422 (setq exclude-areas
1416 (nreverse 1423 (nreverse
1417 (sort exclude-areas #'car-less-than-car)))) 1424 (sort exclude-areas #'car-less-than-car))))
@@ -1458,14 +1465,17 @@ aligner would have dealt with are."
1458 (setq same nil) 1465 (setq same nil)
1459 (align--set-marker eol (line-end-position))) 1466 (align--set-marker eol (line-end-position)))
1460 1467
1461 ;; remember the beginning position of this rule 1468 ;; Remember the beginning position of this rule
1462 ;; match, and save the match-data, since either 1469 ;; match as a marker so it remains accurate after
1463 ;; the `valid' form, or the code that searches for 1470 ;; `align-regions' modifies the buffer for a
1464 ;; section separation, might alter it 1471 ;; previous alignment section. Also save the
1465 (setq rule-beg (match-beginning first) 1472 ;; match-data, since either the `valid' form, or
1466 save-match-data (match-data)) 1473 ;; the code that searches for section separation,
1467 1474 ;; might alter it.
1468 (or rule-beg 1475 (align--set-marker rule-beg (match-beginning first) t)
1476 (setq save-match-data (match-data))
1477
1478 (or (marker-position rule-beg)
1469 (error "No match for subexpression %s" first)) 1479 (error "No match for subexpression %s" first))
1470 1480
1471 ;; unless the `valid' attribute is set, and tells 1481 ;; unless the `valid' attribute is set, and tells
@@ -1480,6 +1490,18 @@ aligner would have dealt with are."
1480 (when (and last-point 1490 (when (and last-point
1481 (align-new-section-p last-point rule-beg 1491 (align-new-section-p last-point rule-beg
1482 thissep)) 1492 thissep))
1493 ;; Convert saved match-data positions to
1494 ;; markers before `align-regions' modifies
1495 ;; the buffer, so the restored match-data
1496 ;; reflects the updated buffer state.
1497 (setq save-match-data
1498 (mapcar (lambda (pos)
1499 (if (integerp pos)
1500 (let ((m (copy-marker pos)))
1501 (push m markers)
1502 m)
1503 pos))
1504 save-match-data))
1483 (align-regions regions align-props rule func) 1505 (align-regions regions align-props rule func)
1484 (setq regions nil) 1506 (setq regions nil)
1485 (setq align-props nil)) 1507 (setq align-props nil))
diff --git a/lisp/battery.el b/lisp/battery.el
index 05f9c5ecadb..ee3e24b196c 100644
--- a/lisp/battery.el
+++ b/lisp/battery.el
@@ -207,8 +207,14 @@ The full `format-spec' formatting syntax is supported."
207 :type '(choice string (const nil))) 207 :type '(choice string (const nil)))
208 208
209(defcustom battery-update-interval 60 209(defcustom battery-update-interval 60
210 "Seconds after which the battery status will be updated." 210 "Seconds after which the battery status will be updated.
211 :type 'integer) 211A value of nil means do not poll for battery status changes.
212This can be useful when `battery-status-function' is set to
213`battery-upower' and `battery-upower-subscribe' is non-nil, in
214which case D-Bus automatically signals battery status changes."
215 :version "31.1"
216 :type '(choice (const :tag "Never" nil)
217 (integer :tag "Number of seconds")))
212 218
213(defcustom battery-load-low 25 219(defcustom battery-load-low 25
214 "Upper bound of low battery load percentage. 220 "Upper bound of low battery load percentage.
@@ -305,8 +311,9 @@ trigger actions based on battery-related events."
305 (and (eq battery-status-function #'battery-upower) 311 (and (eq battery-status-function #'battery-upower)
306 battery-upower-subscribe 312 battery-upower-subscribe
307 (battery--upower-subscribe)) 313 (battery--upower-subscribe))
308 (setq battery-update-timer (run-at-time nil battery-update-interval 314 (when battery-update-interval
309 #'battery-update-handler)) 315 (setq battery-update-timer (run-at-time nil battery-update-interval
316 #'battery-update-handler)))
310 (battery-update)) 317 (battery-update))
311 (message "Battery status not available") 318 (message "Battery status not available")
312 (setq display-battery-mode nil))) 319 (setq display-battery-mode nil)))
@@ -772,17 +779,37 @@ See URL `https://upower.freedesktop.org/docs/Device.html'.")
772(defconst battery-upower-device-path "/org/freedesktop/UPower/devices" 779(defconst battery-upower-device-path "/org/freedesktop/UPower/devices"
773 "D-Bus object providing `battery-upower-device-interface'.") 780 "D-Bus object providing `battery-upower-device-interface'.")
774 781
782(defconst battery-upower-display-device-path
783 "/org/freedesktop/UPower/devices/DisplayDevice"
784 "D-Bus object providing a subset of `battery-upower-device-interface'.
785This is a composite device for displaying a digest of overall state.
786In particular, it is not listed by the EnumerateDevices method.")
787
788(defvar battery-upower-subscribe-properties
789 '(;; `battery-upower-path' properties.
790 "OnBattery"
791 ;; `battery-upower-display-device-path' properties.
792 "State" "Percentage" "IsPresent")
793 "List of UPower device properties to listen for.
794Each value is a string property of `battery-upower-path'
795or `battery-upower-display-device-path'.
796A D-Bus signal that any of them changed results in a `battery-update'.")
797
775(defvar battery--upower-signals nil 798(defvar battery--upower-signals nil
776 "Handles for UPower signal subscriptions.") 799 "Handles for UPower signal subscriptions.")
777 800
778(defun battery--upower-signal-handler (&rest _) 801(defun battery--upower-signal-handler (&rest _)
779 "Update battery status on receiving a UPower D-Bus signal." 802 "Update battery status on receiving a UPower D-Bus signal."
780 (timer-event-handler battery-update-timer)) 803 (if battery-update-timer
804 (timer-event-handler battery-update-timer)
805 (battery-update-handler)))
781 806
782(defun battery--upower-props-changed (_interface changed _invalidated) 807(defun battery--upower-props-changed (_interface changed _invalidated)
783 "Update status when system starts/stops running on battery. 808 "Update status when UPower device properties change.
809Respond only to those in `battery-upower-subscribe-properties'.
784Intended as a UPower PropertiesChanged signal handler." 810Intended as a UPower PropertiesChanged signal handler."
785 (when (assoc "OnBattery" changed) 811 (when (any (lambda (prop) (assoc prop changed))
812 battery-upower-subscribe-properties)
786 (battery--upower-signal-handler))) 813 (battery--upower-signal-handler)))
787 814
788(defun battery--upower-unsubscribe () 815(defun battery--upower-unsubscribe ()
@@ -792,12 +819,20 @@ Intended as a UPower PropertiesChanged signal handler."
792 819
793(defun battery--upower-subscribe () 820(defun battery--upower-subscribe ()
794 "Subscribe to UPower device change signals." 821 "Subscribe to UPower device change signals."
822 ;; Listen for OnBattery changes.
795 (push (dbus-register-signal :system battery-upower-service 823 (push (dbus-register-signal :system battery-upower-service
796 battery-upower-path 824 battery-upower-path
797 dbus-interface-properties 825 dbus-interface-properties
798 "PropertiesChanged" 826 "PropertiesChanged"
799 #'battery--upower-props-changed) 827 #'battery--upower-props-changed)
800 battery--upower-signals) 828 battery--upower-signals)
829 ;; Listen for DisplayDevice property changes.
830 (push (dbus-register-signal :system battery-upower-service
831 battery-upower-display-device-path
832 dbus-interface-properties
833 "PropertiesChanged"
834 #'battery--upower-props-changed)
835 battery--upower-signals)
801 (dolist (method '("DeviceAdded" "DeviceRemoved")) 836 (dolist (method '("DeviceAdded" "DeviceRemoved"))
802 (push (dbus-register-signal :system battery-upower-service 837 (push (dbus-register-signal :system battery-upower-service
803 battery-upower-path 838 battery-upower-path
@@ -879,8 +914,10 @@ The following %-sequences are provided:
879 ((and (eq type 1) (not (eq line-status 'online))) 914 ((and (eq type 1) (not (eq line-status 'online)))
880 ;; It's a line power device: `online' if currently providing 915 ;; It's a line power device: `online' if currently providing
881 ;; power, any other non-nil value if simply present. 916 ;; power, any other non-nil value if simply present.
882 (setq line-status (if (cdr (assoc "Online" props)) 'online t))) 917 (setq line-status (or (not (cdr (assoc "Online" props))) 'online)))
883 ((and (eq type 2) (cdr (assoc "IsPresent" props))) 918 ((and (eq type 2)
919 (cdr (assoc "PowerSupply" props))
920 (cdr (assoc "IsPresent" props)))
884 ;; It's a battery. 921 ;; It's a battery.
885 (setq count (1+ count)) 922 (setq count (1+ count))
886 (setq state (battery--upower-state props state)) 923 (setq state (battery--upower-state props state))
diff --git a/lisp/calendar/appt.el b/lisp/calendar/appt.el
index d6b8621d26b..df8e28319e5 100644
--- a/lisp/calendar/appt.el
+++ b/lisp/calendar/appt.el
@@ -81,8 +81,7 @@
81(defcustom appt-message-warning-time 12 81(defcustom appt-message-warning-time 12
82 "Default time in minutes before an appointment that the warning begins. 82 "Default time in minutes before an appointment that the warning begins.
83You probably want to make `appt-display-interval' a factor of this." 83You probably want to make `appt-display-interval' a factor of this."
84 :type 'integer 84 :type 'integer)
85 :group 'appt)
86 85
87(defcustom appt-warning-time-regexp "warntime \\([0-9]+\\)" 86(defcustom appt-warning-time-regexp "warntime \\([0-9]+\\)"
88 "Regexp matching a string giving the warning time for an appointment. 87 "Regexp matching a string giving the warning time for an appointment.
@@ -92,13 +91,11 @@ You may want to put this inside a diary comment (see `diary-comment-start').
92For example, to be warned 30 minutes in advance of an appointment: 91For example, to be warned 30 minutes in advance of an appointment:
93 2011/06/01 12:00 Do something ## warntime 30" 92 2011/06/01 12:00 Do something ## warntime 30"
94 :version "24.1" 93 :version "24.1"
95 :type 'regexp 94 :type 'regexp)
96 :group 'appt)
97 95
98(defcustom appt-audible t 96(defcustom appt-audible t
99 "Non-nil means beep to indicate appointment." 97 "Non-nil means beep to indicate appointment."
100 :type 'boolean 98 :type 'boolean)
101 :group 'appt)
102 99
103;; TODO - add popup. 100;; TODO - add popup.
104(defcustom appt-display-format 'window 101(defcustom appt-display-format 'window
@@ -112,7 +109,6 @@ See also `appt-audible' and `appt-display-mode-line'."
112 (const :tag "Separate window" window) 109 (const :tag "Separate window" window)
113 (const :tag "Echo-area" echo) 110 (const :tag "Echo-area" echo)
114 (const :tag "No visible display" nil)) 111 (const :tag "No visible display" nil))
115 :group 'appt
116 :version "24.1") ; no longer inherit from deleted obsolete variables 112 :version "24.1") ; no longer inherit from deleted obsolete variables
117 113
118(defcustom appt-display-mode-line t 114(defcustom appt-display-mode-line t
@@ -120,21 +116,18 @@ See also `appt-audible' and `appt-display-mode-line'."
120This is in addition to any other display of appointment messages. 116This is in addition to any other display of appointment messages.
121The mode line updates every minute, independent of the value of 117The mode line updates every minute, independent of the value of
122`appt-display-interval'." 118`appt-display-interval'."
123 :type 'boolean 119 :type 'boolean)
124 :group 'appt)
125 120
126(defcustom appt-display-duration 10 121(defcustom appt-display-duration 10
127 "The number of seconds an appointment message is displayed. 122 "The number of seconds an appointment message is displayed.
128Only relevant if reminders are to be displayed in their own window." 123Only relevant if reminders are to be displayed in their own window."
129 :type 'integer 124 :type 'integer)
130 :group 'appt)
131 125
132(defcustom appt-display-diary t 126(defcustom appt-display-diary t
133 "Non-nil displays the diary when the appointment list is first initialized. 127 "Non-nil displays the diary when the appointment list is first initialized.
134This occurs when this package is first activated, and then at 128This occurs when this package is first activated, and then at
135midnight when the appointment list updates." 129midnight when the appointment list updates."
136 :type 'boolean 130 :type 'boolean)
137 :group 'appt)
138 131
139(defcustom appt-display-interval 3 132(defcustom appt-display-interval 3
140 "Interval in minutes at which to display appointment reminders. 133 "Interval in minutes at which to display appointment reminders.
@@ -146,8 +139,7 @@ a final message displayed precisely when the appointment is due.
146Note that this variable controls the interval at which 139Note that this variable controls the interval at which
147`appt-display-message' is called. The mode line display (if active) 140`appt-display-message' is called. The mode line display (if active)
148always updates every minute." 141always updates every minute."
149 :type 'integer 142 :type 'integer)
150 :group 'appt)
151 143
152(defcustom appt-disp-window-function #'appt-disp-window 144(defcustom appt-disp-window-function #'appt-disp-window
153 "Function called to display appointment window. 145 "Function called to display appointment window.
@@ -156,14 +148,12 @@ It should take three string arguments: the number of minutes till
156the appointment, the current time, and the text of the appointment. 148the appointment, the current time, and the text of the appointment.
157Each argument may also be a list, if multiple appointments are 149Each argument may also be a list, if multiple appointments are
158relevant at any one time." 150relevant at any one time."
159 :type 'function 151 :type 'function)
160 :group 'appt)
161 152
162(defcustom appt-delete-window-function #'appt-delete-window 153(defcustom appt-delete-window-function #'appt-delete-window
163 "Function called to remove appointment window and buffer. 154 "Function called to remove appointment window and buffer.
164Only relevant if reminders are being displayed in a window." 155Only relevant if reminders are being displayed in a window."
165 :type 'function 156 :type 'function)
166 :group 'appt)
167 157
168(defface appt-notification 158(defface appt-notification
169 '((t :inherit mode-line-emphasis)) 159 '((t :inherit mode-line-emphasis))
@@ -602,7 +592,7 @@ Any appointments made with `appt-add' are not affected by this function."
602 (not (eq diary-number-of-entries 1)) 592 (not (eq diary-number-of-entries 1))
603 (not (memq (car (last diary-list-entries-hook)) 593 (not (memq (car (last diary-list-entries-hook))
604 '(diary-sort-entries sort-diary-entries))) 594 '(diary-sort-entries sort-diary-entries)))
605 (setq entry-list (sort entry-list 'diary-entry-compare))) 595 (setq entry-list (sort entry-list #'diary-entry-compare)))
606 ;; Skip diary entries for dates before today. 596 ;; Skip diary entries for dates before today.
607 (while (and entry-list 597 (while (and entry-list
608 (calendar-date-compare 598 (calendar-date-compare
diff --git a/lisp/calendar/cal-bahai.el b/lisp/calendar/cal-bahai.el
index ad0379bb731..8afa4046c4e 100644
--- a/lisp/calendar/cal-bahai.el
+++ b/lisp/calendar/cal-bahai.el
@@ -368,8 +368,7 @@ Reads a year, month and day."
368 (month (cdr (assoc 368 (month (cdr (assoc
369 (completing-read 369 (completing-read
370 "Bahá’í calendar month name: " 370 "Bahá’í calendar month name: "
371 (mapcar 'list 371 (append calendar-bahai-month-name-array nil)
372 (append calendar-bahai-month-name-array nil))
373 nil t) 372 nil t)
374 (calendar-make-alist calendar-bahai-month-name-array 373 (calendar-make-alist calendar-bahai-month-name-array
375 1)))) 374 1))))
diff --git a/lisp/calendar/cal-china.el b/lisp/calendar/cal-china.el
index af470030499..cadbd6f937f 100644
--- a/lisp/calendar/cal-china.el
+++ b/lisp/calendar/cal-china.el
@@ -65,8 +65,7 @@
65 "Minutes difference between local standard time for Chinese calendar and UTC. 65 "Minutes difference between local standard time for Chinese calendar and UTC.
66Default is for Beijing. This is an expression in `year' since it changed at 66Default is for Beijing. This is an expression in `year' since it changed at
671928-01-01 00:00:00 from UT+7:45:40 to UT+8." 671928-01-01 00:00:00 from UT+7:45:40 to UT+8."
68 :type 'sexp 68 :type 'sexp)
69 :group 'calendar-chinese)
70 69
71;; It gets eval'd. 70;; It gets eval'd.
72;;;###autoload 71;;;###autoload
@@ -75,8 +74,7 @@ Default is for Beijing. This is an expression in `year' since it changed at
75;; FIXME unused. 74;; FIXME unused.
76(defcustom calendar-chinese-location-name "Beijing" 75(defcustom calendar-chinese-location-name "Beijing"
77 "Name of location used for calculation of Chinese calendar." 76 "Name of location used for calculation of Chinese calendar."
78 :type 'string 77 :type 'string)
79 :group 'calendar-chinese)
80 78
81(defcustom calendar-chinese-daylight-time-offset 0 79(defcustom calendar-chinese-daylight-time-offset 0
82;; The correct value is as follows, but the Chinese calendrical 80;; The correct value is as follows, but the Chinese calendrical
@@ -84,8 +82,7 @@ Default is for Beijing. This is an expression in `year' since it changed at
84;; 60 82;; 60
85 "Minutes difference between daylight saving and standard time. 83 "Minutes difference between daylight saving and standard time.
86Default is for no daylight saving time." 84Default is for no daylight saving time."
87 :type 'integer 85 :type 'integer)
88 :group 'calendar-chinese)
89 86
90(defcustom calendar-chinese-standard-time-zone-name 87(defcustom calendar-chinese-standard-time-zone-name
91 '(if (< year 1928) 88 '(if (< year 1928)
@@ -95,13 +92,11 @@ Default is for no daylight saving time."
95This is an expression depending on `year' because it changed 92This is an expression depending on `year' because it changed
96at 1928-01-01 00:00:00 from `PMT' to `CST'." 93at 1928-01-01 00:00:00 from `PMT' to `CST'."
97 :type 'sexp 94 :type 'sexp
98 :risky t 95 :risky t)
99 :group 'calendar-chinese)
100 96
101(defcustom calendar-chinese-daylight-time-zone-name "CDT" 97(defcustom calendar-chinese-daylight-time-zone-name "CDT"
102 "Abbreviated name of daylight saving time zone used for Chinese calendar." 98 "Abbreviated name of daylight saving time zone used for Chinese calendar."
103 :type 'string 99 :type 'string)
104 :group 'calendar-chinese)
105 100
106(defcustom calendar-chinese-daylight-saving-start nil 101(defcustom calendar-chinese-daylight-saving-start nil
107;; The correct value is as follows, but the Chinese calendrical 102;; The correct value is as follows, but the Chinese calendrical
@@ -113,8 +108,7 @@ at 1928-01-01 00:00:00 from `PMT' to `CST'."
113Default is for no daylight saving time. See documentation of 108Default is for no daylight saving time. See documentation of
114`calendar-daylight-savings-starts'." 109`calendar-daylight-savings-starts'."
115 :type 'sexp 110 :type 'sexp
116 :risky t 111 :risky t)
117 :group 'calendar-chinese)
118 112
119(defcustom calendar-chinese-daylight-saving-end nil 113(defcustom calendar-chinese-daylight-saving-end nil
120;; The correct value is as follows, but the Chinese calendrical 114;; The correct value is as follows, but the Chinese calendrical
@@ -124,25 +118,21 @@ Default is for no daylight saving time. See documentation of
124Default is for no daylight saving time. See documentation of 118Default is for no daylight saving time. See documentation of
125`calendar-daylight-savings-ends'." 119`calendar-daylight-savings-ends'."
126 :type 'sexp 120 :type 'sexp
127 :risky t 121 :risky t)
128 :group 'calendar-chinese)
129 122
130(defcustom calendar-chinese-daylight-saving-start-time 0 123(defcustom calendar-chinese-daylight-saving-start-time 0
131 "Number of minutes after midnight that daylight saving time starts. 124 "Number of minutes after midnight that daylight saving time starts.
132Default is for no daylight saving time." 125Default is for no daylight saving time."
133 :type 'integer 126 :type 'integer)
134 :group 'calendar-chinese)
135 127
136(defcustom calendar-chinese-daylight-saving-end-time 0 128(defcustom calendar-chinese-daylight-saving-end-time 0
137 "Number of minutes after midnight that daylight saving time ends. 129 "Number of minutes after midnight that daylight saving time ends.
138Default is for no daylight saving time." 130Default is for no daylight saving time."
139 :type 'integer 131 :type 'integer)
140 :group 'calendar-chinese)
141 132
142(defcustom calendar-chinese-celestial-stem 133(defcustom calendar-chinese-celestial-stem
143 ["Jia" "Yi" "Bing" "Ding" "Wu" "Ji" "Geng" "Xin" "Ren" "Gui"] 134 ["Jia" "Yi" "Bing" "Ding" "Wu" "Ji" "Geng" "Xin" "Ren" "Gui"]
144 "Prefixes used by `calendar-chinese-sexagesimal-name'." 135 "Prefixes used by `calendar-chinese-sexagesimal-name'."
145 :group 'calendar-chinese
146 :type '(vector (string :tag "Jia") 136 :type '(vector (string :tag "Jia")
147 (string :tag "Yi") 137 (string :tag "Yi")
148 (string :tag "Bing") 138 (string :tag "Bing")
@@ -157,7 +147,6 @@ Default is for no daylight saving time."
157(defcustom calendar-chinese-terrestrial-branch 147(defcustom calendar-chinese-terrestrial-branch
158 ["Zi" "Chou" "Yin" "Mao" "Chen" "Si" "Wu" "Wei" "Shen" "You" "Xu" "Hai"] 148 ["Zi" "Chou" "Yin" "Mao" "Chen" "Si" "Wu" "Wei" "Shen" "You" "Xu" "Hai"]
159 "Suffixes used by `calendar-chinese-sexagesimal-name'." 149 "Suffixes used by `calendar-chinese-sexagesimal-name'."
160 :group 'calendar-chinese
161 :type '(vector (string :tag "Zi") 150 :type '(vector (string :tag "Zi")
162 (string :tag "Chou") 151 (string :tag "Chou")
163 (string :tag "Yin") 152 (string :tag "Yin")
@@ -188,7 +177,7 @@ The Zodiac signs begin when the sun's longitude is a multiple of 30 degrees."
188 (with-suppressed-warnings ((lexical year)) 177 (with-suppressed-warnings ((lexical year))
189 (defvar year)) 178 (defvar year))
190 (let* ((year (calendar-extract-year (calendar-gregorian-from-absolute d))) 179 (let* ((year (calendar-extract-year (calendar-gregorian-from-absolute d)))
191 (calendar-time-zone (eval calendar-chinese-time-zone)) ; uses year 180 (calendar-time-zone (eval calendar-chinese-time-zone t)) ; uses year
192 (calendar-daylight-time-offset 181 (calendar-daylight-time-offset
193 calendar-chinese-daylight-time-offset) 182 calendar-chinese-daylight-time-offset)
194 (calendar-standard-time-zone-name 183 (calendar-standard-time-zone-name
@@ -212,7 +201,7 @@ The Zodiac signs begin when the sun's longitude is a multiple of 30 degrees."
212 (with-suppressed-warnings ((lexical year)) 201 (with-suppressed-warnings ((lexical year))
213 (defvar year)) 202 (defvar year))
214 (let* ((year (calendar-extract-year (calendar-gregorian-from-absolute d))) 203 (let* ((year (calendar-extract-year (calendar-gregorian-from-absolute d)))
215 (calendar-time-zone (eval calendar-chinese-time-zone)) 204 (calendar-time-zone (eval calendar-chinese-time-zone t))
216 (calendar-daylight-time-offset 205 (calendar-daylight-time-offset
217 calendar-chinese-daylight-time-offset) 206 calendar-chinese-daylight-time-offset)
218 (calendar-standard-time-zone-name 207 (calendar-standard-time-zone-name
diff --git a/lisp/calendar/cal-coptic.el b/lisp/calendar/cal-coptic.el
index 9696a484224..2878c35bb7c 100644
--- a/lisp/calendar/cal-coptic.el
+++ b/lisp/calendar/cal-coptic.el
@@ -148,8 +148,7 @@ Reads a year, month, and day."
148 (month (cdr (assoc-string 148 (month (cdr (assoc-string
149 (completing-read 149 (completing-read
150 (format "%s calendar month name: " calendar-coptic-name) 150 (format "%s calendar month name: " calendar-coptic-name)
151 (mapcar 'list 151 (append calendar-coptic-month-name-array nil)
152 (append calendar-coptic-month-name-array nil))
153 nil t) 152 nil t)
154 (calendar-make-alist calendar-coptic-month-name-array 153 (calendar-make-alist calendar-coptic-month-name-array
155 1) 154 1)
diff --git a/lisp/calendar/cal-dst.el b/lisp/calendar/cal-dst.el
index ca95fb59607..7681059f592 100644
--- a/lisp/calendar/cal-dst.el
+++ b/lisp/calendar/cal-dst.el
@@ -46,8 +46,7 @@ current date apply to all years. This is faster, but not always
46correct, since the dates of daylight saving transitions sometimes 46correct, since the dates of daylight saving transitions sometimes
47change." 47change."
48 :type 'boolean 48 :type 'boolean
49 :version "22.1" 49 :version "22.1")
50 :group 'calendar-dst)
51 50
52;;;###autoload 51;;;###autoload
53(put 'calendar-daylight-savings-starts 'risky-local-variable t) 52(put 'calendar-daylight-savings-starts 'risky-local-variable t)
@@ -68,8 +67,7 @@ If it starts on the first Sunday in April, you would set it to
68 (calendar-nth-named-day 1 0 4 year) 67 (calendar-nth-named-day 1 0 4 year)
69 68
70If the locale never uses daylight saving time, set this to nil." 69If the locale never uses daylight saving time, set this to nil."
71 :type 'sexp 70 :type 'sexp)
72 :group 'calendar-dst)
73 71
74;;;###autoload 72;;;###autoload
75(put 'calendar-daylight-savings-ends 'risky-local-variable t) 73(put 'calendar-daylight-savings-ends 'risky-local-variable t)
@@ -85,8 +83,7 @@ For example, if daylight saving time ends on the last Sunday in October:
85 (calendar-nth-named-day -1 0 10 year) 83 (calendar-nth-named-day -1 0 10 year)
86 84
87If the locale never uses daylight saving time, set this to nil." 85If the locale never uses daylight saving time, set this to nil."
88 :type 'sexp 86 :type 'sexp)
89 :group 'calendar-dst)
90 87
91;;; More defcustoms below. 88;;; More defcustoms below.
92 89
@@ -208,10 +205,12 @@ The result has the proper form for `calendar-daylight-savings-starts'."
208 ;; we require an absolute date. The following is for efficiency. 205 ;; we require an absolute date. The following is for efficiency.
209 (setq date (cond ((eq (car rule) #'calendar-nth-named-day) 206 (setq date (cond ((eq (car rule) #'calendar-nth-named-day)
210 (eval (cons #'calendar-nth-named-absday 207 (eval (cons #'calendar-nth-named-absday
211 (cdr rule)))) 208 (cdr rule))
209 t))
212 ((eq (car rule) #'calendar-gregorian-from-absolute) 210 ((eq (car rule) #'calendar-gregorian-from-absolute)
213 (eval (cadr rule))) 211 (eval (cadr rule) t))
214 (t (calendar-absolute-from-gregorian (eval rule))))) 212 (t (calendar-absolute-from-gregorian
213 (eval rule t)))))
215 (or (equal (current-time-zone 214 (or (equal (current-time-zone
216 (calendar-time-from-absolute date prevday-sec)) 215 (calendar-time-from-absolute date prevday-sec))
217 (current-time-zone 216 (current-time-zone
@@ -226,7 +225,7 @@ The result has the proper form for `calendar-daylight-savings-starts'."
226 (car candidate-rules))) 225 (car candidate-rules)))
227 226
228;; TODO it might be better to extract this information directly from 227;; TODO it might be better to extract this information directly from
229;; the system timezone database. But cross-platform...? 228;; the system timezone database. But cross-platform...?
230;; See thread 229;; See thread
231;; https://lists.gnu.org/r/emacs-pretest-bug/2006-11/msg00060.html 230;; https://lists.gnu.org/r/emacs-pretest-bug/2006-11/msg00060.html
232(defun calendar-dst-find-data (&optional time) 231(defun calendar-dst-find-data (&optional time)
@@ -309,7 +308,9 @@ system knows:
309UTC-DIFF is an integer specifying the number of minutes difference between 308UTC-DIFF is an integer specifying the number of minutes difference between
310 standard time in the current time zone and Coordinated Universal Time 309 standard time in the current time zone and Coordinated Universal Time
311 (Greenwich Mean Time). A negative value means west of Greenwich. 310 (Greenwich Mean Time). A negative value means west of Greenwich.
312DST-OFFSET is an integer giving the daylight saving time offset in minutes. 311DST-OFFSET is an integer giving the daylight saving time offset in minutes
312 relative to UTC-DIFF. (That is, the total UTC offset during daylight saving
313 time is UTC-DIFF + DST-OFFSET minutes.)
313STD-ZONE is a string giving the name of the time zone when no seasonal time 314STD-ZONE is a string giving the name of the time zone when no seasonal time
314 adjustment is in effect. 315 adjustment is in effect.
315DST-ZONE is a string giving the name of the time zone when there is a seasonal 316DST-ZONE is a string giving the name of the time zone when there is a seasonal
@@ -339,15 +340,13 @@ it can't find."
339(defcustom calendar-time-zone (or (car calendar-current-time-zone-cache) -300) 340(defcustom calendar-time-zone (or (car calendar-current-time-zone-cache) -300)
340 "Number of minutes difference between local standard time and UTC. 341 "Number of minutes difference between local standard time and UTC.
341For example, -300 for New York City, -480 for Los Angeles." 342For example, -300 for New York City, -480 for Los Angeles."
342 :type 'integer 343 :type 'integer)
343 :group 'calendar-dst)
344 344
345(defcustom calendar-daylight-time-offset 345(defcustom calendar-daylight-time-offset
346 (or (cadr calendar-current-time-zone-cache) 60) 346 (or (cadr calendar-current-time-zone-cache) 60)
347 "Number of minutes difference between daylight saving and standard time. 347 "Number of minutes difference between daylight saving and standard time.
348If the locale never uses daylight saving time, set this to 0." 348If the locale never uses daylight saving time, set this to 0."
349 :type 'integer 349 :type 'integer)
350 :group 'calendar-dst)
351 350
352(defcustom calendar-standard-time-zone-name 351(defcustom calendar-standard-time-zone-name
353 (if (eq calendar-time-zone-style 'numeric) 352 (if (eq calendar-time-zone-style 'numeric)
@@ -360,8 +359,7 @@ If the locale never uses daylight saving time, set this to 0."
360For example, \"-0500\" or \"EST\" in New York City." 359For example, \"-0500\" or \"EST\" in New York City."
361 :type 'string 360 :type 'string
362 :version "28.1" 361 :version "28.1"
363 :set-after '(calendar-time-zone-style) 362 :set-after '(calendar-time-zone-style))
364 :group 'calendar-dst)
365 363
366(defcustom calendar-daylight-time-zone-name 364(defcustom calendar-daylight-time-zone-name
367 (if (eq calendar-time-zone-style 'numeric) 365 (if (eq calendar-time-zone-style 'numeric)
@@ -374,21 +372,18 @@ For example, \"-0500\" or \"EST\" in New York City."
374For example, \"-0400\" or \"EDT\" in New York City." 372For example, \"-0400\" or \"EDT\" in New York City."
375 :type 'string 373 :type 'string
376 :version "28.1" 374 :version "28.1"
377 :set-after '(calendar-time-zone-style) 375 :set-after '(calendar-time-zone-style))
378 :group 'calendar-dst)
379 376
380(defcustom calendar-daylight-savings-starts-time 377(defcustom calendar-daylight-savings-starts-time
381 (or (nth 6 calendar-current-time-zone-cache) 120) 378 (or (nth 6 calendar-current-time-zone-cache) 120)
382 "Number of minutes after midnight that daylight saving time starts." 379 "Number of minutes after midnight that daylight saving time starts."
383 :type 'integer 380 :type 'integer)
384 :group 'calendar-dst)
385 381
386(defcustom calendar-daylight-savings-ends-time 382(defcustom calendar-daylight-savings-ends-time
387 (or (nth 7 calendar-current-time-zone-cache) 383 (or (nth 7 calendar-current-time-zone-cache)
388 calendar-daylight-savings-starts-time) 384 calendar-daylight-savings-starts-time)
389 "Number of minutes after midnight that daylight saving time ends." 385 "Number of minutes after midnight that daylight saving time ends."
390 :type 'integer 386 :type 'integer)
391 :group 'calendar-dst)
392 387
393 388
394(defun calendar-dst-starts (year) 389(defun calendar-dst-starts (year)
@@ -398,7 +393,7 @@ This function respects the value of `calendar-dst-check-each-year-flag'."
398 (cadr (calendar-dst-find-startend year)) 393 (cadr (calendar-dst-find-startend year))
399 (nth 4 calendar-current-time-zone-cache)))) 394 (nth 4 calendar-current-time-zone-cache))))
400 (calendar-dlet ((year year)) 395 (calendar-dlet ((year year))
401 (if expr (eval expr)))) 396 (if expr (eval expr t))))
402 ;; New US rules commencing 2007. https://www.iana.org/time-zones 397 ;; New US rules commencing 2007. https://www.iana.org/time-zones
403 (and (not (zerop calendar-daylight-time-offset)) 398 (and (not (zerop calendar-daylight-time-offset))
404 (calendar-nth-named-day 2 0 3 year)))) 399 (calendar-nth-named-day 2 0 3 year))))
@@ -410,7 +405,7 @@ This function respects the value of `calendar-dst-check-each-year-flag'."
410 (nth 2 (calendar-dst-find-startend year)) 405 (nth 2 (calendar-dst-find-startend year))
411 (nth 5 calendar-current-time-zone-cache)))) 406 (nth 5 calendar-current-time-zone-cache))))
412 (calendar-dlet ((year year)) 407 (calendar-dlet ((year year))
413 (if expr (eval expr)))) 408 (if expr (eval expr t))))
414 ;; New US rules commencing 2007. https://www.iana.org/time-zones 409 ;; New US rules commencing 2007. https://www.iana.org/time-zones
415 (and (not (zerop calendar-daylight-time-offset)) 410 (and (not (zerop calendar-daylight-time-offset))
416 (calendar-nth-named-day 1 0 11 year)))) 411 (calendar-nth-named-day 1 0 11 year))))
@@ -421,8 +416,8 @@ This function respects the value of `calendar-dst-check-each-year-flag'."
421Fractional part of DATE is local standard time of day." 416Fractional part of DATE is local standard time of day."
422 (calendar-dlet ((year (calendar-extract-year 417 (calendar-dlet ((year (calendar-extract-year
423 (calendar-gregorian-from-absolute (floor date))))) 418 (calendar-gregorian-from-absolute (floor date)))))
424 (let* ((dst-starts-gregorian (eval calendar-daylight-savings-starts)) 419 (let* ((dst-starts-gregorian (eval calendar-daylight-savings-starts t))
425 (dst-ends-gregorian (eval calendar-daylight-savings-ends)) 420 (dst-ends-gregorian (eval calendar-daylight-savings-ends t))
426 (dst-starts (and dst-starts-gregorian 421 (dst-starts (and dst-starts-gregorian
427 (+ (calendar-absolute-from-gregorian 422 (+ (calendar-absolute-from-gregorian
428 dst-starts-gregorian) 423 dst-starts-gregorian)
diff --git a/lisp/calendar/cal-french.el b/lisp/calendar/cal-french.el
index 061ab4ebafa..28cb2515a86 100644
--- a/lisp/calendar/cal-french.el
+++ b/lisp/calendar/cal-french.el
@@ -344,7 +344,7 @@ Echo French Revolutionary date unless NOECHO is non-nil."
344 (calendar-absolute-from-gregorian 344 (calendar-absolute-from-gregorian
345 (calendar-current-date))))))) 345 (calendar-current-date)))))))
346 (month-list 346 (month-list
347 (mapcar 'list 347 (mapcar #'list
348 (append months 348 (append months
349 (if (calendar-french-leap-year-p year) 349 (if (calendar-french-leap-year-p year)
350 (mapcar #'calendar-french-trim-feast feasts) 350 (mapcar #'calendar-french-trim-feast feasts)
diff --git a/lisp/calendar/cal-hebrew.el b/lisp/calendar/cal-hebrew.el
index 213afc1d3ba..714f18999fa 100644
--- a/lisp/calendar/cal-hebrew.el
+++ b/lisp/calendar/cal-hebrew.el
@@ -238,7 +238,7 @@ Reads a year, month, and day."
238 (month (cdr (assoc-string 238 (month (cdr (assoc-string
239 (completing-read 239 (completing-read
240 "Hebrew calendar month name: " 240 "Hebrew calendar month name: "
241 (mapcar 'list (append month-array nil)) 241 (append month-array nil)
242 (if (= year 3761) 242 (if (= year 3761)
243 (lambda (x) 243 (lambda (x)
244 (let ((m (cdr 244 (let ((m (cdr
@@ -691,7 +691,7 @@ from the cursor position."
691 (month (cdr (assoc-string 691 (month (cdr (assoc-string
692 (completing-read 692 (completing-read
693 "Month of death (name): " 693 "Month of death (name): "
694 (mapcar 'list (append month-array nil)) 694 (append month-array nil)
695 nil t) 695 nil t)
696 (calendar-make-alist month-array 1) t))) 696 (calendar-make-alist month-array 1) t)))
697 (last (calendar-last-day-of-month month year)) 697 (last (calendar-last-day-of-month month year))
@@ -1123,6 +1123,7 @@ use when highlighting the day in the calendar."
1123 1123
1124(declare-function solar-setup "solar" ()) 1124(declare-function solar-setup "solar" ())
1125(declare-function solar-sunrise-sunset "solar" (date)) 1125(declare-function solar-sunrise-sunset "solar" (date))
1126(declare-function solar-time-string "solar" (time time-zone))
1126(defvar calendar-latitude) 1127(defvar calendar-latitude)
1127(defvar calendar-longitude) 1128(defvar calendar-longitude)
1128(defvar calendar-time-zone) 1129(defvar calendar-time-zone)
@@ -1145,7 +1146,7 @@ use when highlighting the day in the calendar."
1145 (if sunset 1146 (if sunset
1146 (cons mark (format 1147 (cons mark (format
1147 "%s Sabbath candle lighting" 1148 "%s Sabbath candle lighting"
1148 (apply 'solar-time-string 1149 (apply #'solar-time-string
1149 (cons (- (car sunset) 1150 (cons (- (car sunset)
1150 (/ diary-hebrew-sabbath-candles-minutes 1151 (/ diary-hebrew-sabbath-candles-minutes
1151 60.0)) 1152 60.0))
diff --git a/lisp/calendar/cal-html.el b/lisp/calendar/cal-html.el
index 06729743243..6e5e42e8f7f 100644
--- a/lisp/calendar/cal-html.el
+++ b/lisp/calendar/cal-html.el
@@ -42,18 +42,15 @@
42 42
43(defcustom cal-html-directory "~/public_html" 43(defcustom cal-html-directory "~/public_html"
44 "Directory for HTML pages generated by cal-html." 44 "Directory for HTML pages generated by cal-html."
45 :type 'string 45 :type 'string)
46 :group 'calendar-html)
47 46
48(defcustom cal-html-print-day-number-flag nil 47(defcustom cal-html-print-day-number-flag nil
49 "Non-nil means print the day-of-the-year number in the monthly cal-html page." 48 "Non-nil means print the day-of-the-year number in the monthly cal-html page."
50 :type 'boolean 49 :type 'boolean)
51 :group 'calendar-html)
52 50
53(defcustom cal-html-year-index-cols 3 51(defcustom cal-html-year-index-cols 3
54 "Number of columns in the cal-html yearly index page." 52 "Number of columns in the cal-html yearly index page."
55 :type 'integer 53 :type 'integer)
56 :group 'calendar-html)
57 54
58(defcustom cal-html-day-abbrev-array calendar-day-abbrev-array 55(defcustom cal-html-day-abbrev-array calendar-day-abbrev-array
59 "Array of seven strings for abbreviated day names (starting with Sunday)." 56 "Array of seven strings for abbreviated day names (starting with Sunday)."
@@ -64,14 +61,12 @@
64 (string :tag "Wed") 61 (string :tag "Wed")
65 (string :tag "Thu") 62 (string :tag "Thu")
66 (string :tag "Fri") 63 (string :tag "Fri")
67 (string :tag "Sat")) 64 (string :tag "Sat")))
68 :group 'calendar-html)
69 65
70(defcustom cal-html-holidays t 66(defcustom cal-html-holidays t
71 "If non-nil, include holidays as well as diary entries." 67 "If non-nil, include holidays as well as diary entries."
72 :version "24.3" 68 :version "24.3"
73 :type 'boolean 69 :type 'boolean)
74 :group 'calendar-html)
75 70
76(defcustom cal-html-css-default 71(defcustom cal-html-css-default
77 (concat 72 (concat
@@ -93,8 +88,7 @@
93 "</STYLE>\n\n") 88 "</STYLE>\n\n")
94 "Default cal-html css style. You can override this with a \"cal.css\" file." 89 "Default cal-html css style. You can override this with a \"cal.css\" file."
95 :type 'string 90 :type 'string
96 :version "24.3" ; added SPAN.HOLIDAY 91 :version "24.3") ; Added SPAN.HOLIDAY.
97 :group 'calendar-html)
98 92
99;;; End customizable variables. 93;;; End customizable variables.
100 94
@@ -317,7 +311,7 @@ There are 12/cols rows of COLS months each."
317Characters are replaced according to `cal-html-html-subst-list'." 311Characters are replaced according to `cal-html-html-subst-list'."
318 (if (stringp string) 312 (if (stringp string)
319 (replace-regexp-in-string 313 (replace-regexp-in-string
320 (regexp-opt (mapcar 'car cal-html-html-subst-list)) 314 (regexp-opt (mapcar #'car cal-html-html-subst-list))
321 (lambda (x) 315 (lambda (x)
322 (cdr (assoc x cal-html-html-subst-list))) 316 (cdr (assoc x cal-html-html-subst-list)))
323 string) 317 string)
diff --git a/lisp/calendar/cal-islam.el b/lisp/calendar/cal-islam.el
index 79cbad2c61a..49da71adac4 100644
--- a/lisp/calendar/cal-islam.el
+++ b/lisp/calendar/cal-islam.el
@@ -154,7 +154,7 @@ Reads a year, month, and day."
154 (month (cdr (assoc-string 154 (month (cdr (assoc-string
155 (completing-read 155 (completing-read
156 "Islamic calendar month name: " 156 "Islamic calendar month name: "
157 (mapcar 'list (append month-array nil)) 157 (append month-array nil)
158 nil t) 158 nil t)
159 (calendar-make-alist month-array 1) t))) 159 (calendar-make-alist month-array 1) t)))
160 (last (calendar-islamic-last-day-of-month month year)) 160 (last (calendar-islamic-last-day-of-month month year))
diff --git a/lisp/calendar/cal-julian.el b/lisp/calendar/cal-julian.el
index 0977b14b2e6..7597f36b62f 100644
--- a/lisp/calendar/cal-julian.el
+++ b/lisp/calendar/cal-julian.el
@@ -107,7 +107,7 @@ Driven by the variable `calendar-date-display-form'."
107 (month (cdr (assoc-string 107 (month (cdr (assoc-string
108 (completing-read 108 (completing-read
109 "Julian calendar month name: " 109 "Julian calendar month name: "
110 (mapcar 'list (append month-array nil)) 110 (append month-array nil)
111 nil t) 111 nil t)
112 (calendar-make-alist month-array 1) t))) 112 (calendar-make-alist month-array 1) t)))
113 (last 113 (last
diff --git a/lisp/calendar/cal-mayan.el b/lisp/calendar/cal-mayan.el
index df4ebe873f9..886e92a859d 100644
--- a/lisp/calendar/cal-mayan.el
+++ b/lisp/calendar/cal-mayan.el
@@ -70,7 +70,7 @@ but some use 1137140. Using 1232041 gives you Spinden's correlation; using
70 70
71(defun calendar-mayan-long-count-to-string (mayan-long-count) 71(defun calendar-mayan-long-count-to-string (mayan-long-count)
72 "Convert MAYAN-LONG-COUNT into traditional written form." 72 "Convert MAYAN-LONG-COUNT into traditional written form."
73 (apply 'format (cons "%s.%s.%s.%s.%s" mayan-long-count))) 73 (apply #'format (cons "%s.%s.%s.%s.%s" mayan-long-count)))
74 74
75(defun calendar-mayan-string-from-long-count (str) 75(defun calendar-mayan-string-from-long-count (str)
76 "Given STR, a string of format \"%d.%d.%d.%d.%d\", return list of numbers." 76 "Given STR, a string of format \"%d.%d.%d.%d.%d\", return list of numbers."
@@ -144,7 +144,7 @@ but some use 1137140. Using 1232041 gives you Spinden's correlation; using
144 (haab-month (cdr 144 (haab-month (cdr
145 (assoc-string 145 (assoc-string
146 (completing-read "Haab uinal: " 146 (completing-read "Haab uinal: "
147 (mapcar 'list haab-month-list) 147 haab-month-list
148 nil t) 148 nil t)
149 (calendar-make-alist haab-month-list 1) t)))) 149 (calendar-make-alist haab-month-list 1) t))))
150 (cons haab-day haab-month))) 150 (cons haab-day haab-month)))
@@ -160,7 +160,7 @@ but some use 1137140. Using 1232041 gives you Spinden's correlation; using
160 (tzolkin-name (cdr 160 (tzolkin-name (cdr
161 (assoc-string 161 (assoc-string
162 (completing-read "Tzolkin uinal: " 162 (completing-read "Tzolkin uinal: "
163 (mapcar 'list tzolkin-name-list) 163 tzolkin-name-list
164 nil t) 164 nil t)
165 (calendar-make-alist tzolkin-name-list 1) t)))) 165 (calendar-make-alist tzolkin-name-list 1) t))))
166 (cons tzolkin-count tzolkin-name))) 166 (cons tzolkin-count tzolkin-name)))
diff --git a/lisp/calendar/cal-menu.el b/lisp/calendar/cal-menu.el
index 68c6abdd8d8..a95c1c882b4 100644
--- a/lisp/calendar/cal-menu.el
+++ b/lisp/calendar/cal-menu.el
@@ -206,7 +206,7 @@ is non-nil."
206 (if holidays 206 (if holidays
207 (list "--shadow-etched-in" "--shadow-etched-in")) 207 (list "--shadow-etched-in" "--shadow-etched-in"))
208 (if diary-entries 208 (if diary-entries
209 (mapcar 'list (apply 'append diary-entries)) 209 (mapcar #'list (apply #'append diary-entries))
210 '("None"))))) 210 '("None")))))
211 (and selection (call-interactively selection)))) 211 (and selection (call-interactively selection))))
212 212
diff --git a/lisp/calendar/cal-move.el b/lisp/calendar/cal-move.el
index c96ae182118..f1137dfda5c 100644
--- a/lisp/calendar/cal-move.el
+++ b/lisp/calendar/cal-move.el
@@ -430,11 +430,7 @@ Interactively, prompt for YEAR and DAY number."
430 (calendar-day-number (calendar-current-date)) 430 (calendar-day-number (calendar-current-date))
431 last))) 431 last)))
432 (list year day))) 432 (list year day)))
433 (calendar-goto-date 433 (calendar-goto-date (calendar-date-from-day-of-year year day))
434 (calendar-gregorian-from-absolute
435 (if (< 0 day)
436 (+ -1 day (calendar-absolute-from-gregorian (list 1 1 year)))
437 (+ 1 day (calendar-absolute-from-gregorian (list 12 31 year))))))
438 (or noecho (calendar-print-day-of-year))) 434 (or noecho (calendar-print-day-of-year)))
439 435
440(provide 'cal-move) 436(provide 'cal-move)
diff --git a/lisp/calendar/cal-persia.el b/lisp/calendar/cal-persia.el
index 9f59a75f952..df0a11160fb 100644
--- a/lisp/calendar/cal-persia.el
+++ b/lisp/calendar/cal-persia.el
@@ -169,8 +169,7 @@ Reads a year, month, and day."
169 (month (cdr (assoc 169 (month (cdr (assoc
170 (completing-read 170 (completing-read
171 "Persian calendar month name: " 171 "Persian calendar month name: "
172 (mapcar 'list 172 (append calendar-persian-month-name-array nil)
173 (append calendar-persian-month-name-array nil))
174 nil t) 173 nil t)
175 (calendar-make-alist calendar-persian-month-name-array 174 (calendar-make-alist calendar-persian-month-name-array
176 1)))) 175 1))))
diff --git a/lisp/calendar/cal-tex.el b/lisp/calendar/cal-tex.el
index bf0ff100be3..166aa4658e2 100644
--- a/lisp/calendar/cal-tex.el
+++ b/lisp/calendar/cal-tex.el
@@ -72,26 +72,22 @@
72 "The days of the week that are displayed on the portrait monthly calendar. 72 "The days of the week that are displayed on the portrait monthly calendar.
73Sunday is 0, Monday is 1, and so on. The default is to print from Sunday to 73Sunday is 0, Monday is 1, and so on. The default is to print from Sunday to
74Saturday. For example, (1 3 5) prints only Monday, Wednesday, Friday." 74Saturday. For example, (1 3 5) prints only Monday, Wednesday, Friday."
75 :type '(repeat integer) 75 :type '(repeat integer))
76 :group 'calendar-tex)
77 76
78(defcustom cal-tex-holidays t 77(defcustom cal-tex-holidays t
79 "Non-nil means holidays are printed in the LaTeX calendars that support it. 78 "Non-nil means holidays are printed in the LaTeX calendars that support it.
80Setting this to nil may speed up calendar generation." 79Setting this to nil may speed up calendar generation."
81 :type 'boolean 80 :type 'boolean)
82 :group 'calendar-tex)
83 81
84(defcustom cal-tex-diary nil 82(defcustom cal-tex-diary nil
85 "Non-nil means diary entries are printed in LaTeX calendars that support it. 83 "Non-nil means diary entries are printed in LaTeX calendars that support it.
86Setting this to nil may speed up calendar generation." 84Setting this to nil may speed up calendar generation."
87 :type 'boolean 85 :type 'boolean)
88 :group 'calendar-tex)
89 86
90(defcustom cal-tex-rules nil 87(defcustom cal-tex-rules nil
91 "Non-nil means pages will be ruled in some LaTeX calendar styles. 88 "Non-nil means pages will be ruled in some LaTeX calendar styles.
92At present, this only affects the daily filofax calendar." 89At present, this only affects the daily filofax calendar."
93 :type 'boolean 90 :type 'boolean)
94 :group 'calendar-tex)
95 91
96(defcustom cal-tex-daily-string 92(defcustom cal-tex-daily-string
97 '(let* ((year (calendar-extract-year date)) 93 '(let* ((year (calendar-extract-year date))
@@ -112,30 +108,25 @@ days remaining. As an example, setting this to
112 (calendar-hebrew-date-string date) 108 (calendar-hebrew-date-string date)
113 109
114will put the Hebrew date at the bottom of each day." 110will put the Hebrew date at the bottom of each day."
115 :type 'sexp 111 :type 'sexp)
116 :group 'calendar-tex)
117 112
118(defcustom cal-tex-buffer "calendar.tex" 113(defcustom cal-tex-buffer "calendar.tex"
119 "The name for the output LaTeX calendar buffer." 114 "The name for the output LaTeX calendar buffer."
120 :type 'string 115 :type 'string)
121 :group 'calendar-tex)
122 116
123(defcustom cal-tex-24 nil 117(defcustom cal-tex-24 nil
124 "Non-nil means use a 24 hour clock in the daily calendar." 118 "Non-nil means use a 24 hour clock in the daily calendar."
125 :type 'boolean 119 :type 'boolean)
126 :group 'calendar-tex)
127 120
128(defcustom cal-tex-daily-start 8 121(defcustom cal-tex-daily-start 8
129 "The first hour of the daily LaTeX calendar page. 122 "The first hour of the daily LaTeX calendar page.
130At present, this only affects `cal-tex-cursor-day'." 123At present, this only affects `cal-tex-cursor-day'."
131 :type 'integer 124 :type 'integer)
132 :group 'calendar-tex)
133 125
134(defcustom cal-tex-daily-end 20 126(defcustom cal-tex-daily-end 20
135 "The last hour of the daily LaTeX calendar page. 127 "The last hour of the daily LaTeX calendar page.
136At present, this only affects `cal-tex-cursor-day'." 128At present, this only affects `cal-tex-cursor-day'."
137 :type 'integer 129 :type 'integer)
138 :group 'calendar-tex)
139 130
140(defcustom cal-tex-preamble-extra nil 131(defcustom cal-tex-preamble-extra nil
141 "A string giving extra LaTeX commands to insert in the calendar preamble. 132 "A string giving extra LaTeX commands to insert in the calendar preamble.
@@ -144,7 +135,6 @@ For example, to include extra packages:
144 :type '(choice (const nil) 135 :type '(choice (const nil)
145 ;; An example to help people format things in custom. 136 ;; An example to help people format things in custom.
146 (string :value "\\usepackage{foo}\n\\usepackage{bar}\n")) 137 (string :value "\\usepackage{foo}\n\\usepackage{bar}\n"))
147 :group 'calendar-tex
148 :version "22.1") 138 :version "22.1")
149 139
150(defcustom cal-tex-hook nil 140(defcustom cal-tex-hook nil
@@ -153,28 +143,23 @@ You can use this to do post-processing on the buffer. For example, to change
153characters with diacritical marks to their LaTeX equivalents, use 143characters with diacritical marks to their LaTeX equivalents, use
154 (add-hook \\='cal-tex-hook 144 (add-hook \\='cal-tex-hook
155 (lambda () (iso-iso2tex (point-min) (point-max))))" 145 (lambda () (iso-iso2tex (point-min) (point-max))))"
156 :type 'hook 146 :type 'hook)
157 :group 'calendar-tex)
158 147
159(defcustom cal-tex-year-hook nil 148(defcustom cal-tex-year-hook nil
160 "List of functions called after a LaTeX year calendar buffer is generated." 149 "List of functions called after a LaTeX year calendar buffer is generated."
161 :type 'hook 150 :type 'hook)
162 :group 'calendar-tex)
163 151
164(defcustom cal-tex-month-hook nil 152(defcustom cal-tex-month-hook nil
165 "List of functions called after a LaTeX month calendar buffer is generated." 153 "List of functions called after a LaTeX month calendar buffer is generated."
166 :type 'hook 154 :type 'hook)
167 :group 'calendar-tex)
168 155
169(defcustom cal-tex-week-hook nil 156(defcustom cal-tex-week-hook nil
170 "List of functions called after a LaTeX week calendar buffer is generated." 157 "List of functions called after a LaTeX week calendar buffer is generated."
171 :type 'hook 158 :type 'hook)
172 :group 'calendar-tex)
173 159
174(defcustom cal-tex-daily-hook nil 160(defcustom cal-tex-daily-hook nil
175 "List of functions called after a LaTeX daily calendar buffer is generated." 161 "List of functions called after a LaTeX daily calendar buffer is generated."
176 :type 'hook 162 :type 'hook)
177 :group 'calendar-tex)
178 163
179;;; 164;;;
180;;; Definitions for LaTeX code 165;;; Definitions for LaTeX code
@@ -1227,7 +1212,7 @@ shown are hard-coded to 8-12, 13-17."
1227 (cal-tex-arg (number-to-string (calendar-extract-day date))) 1212 (cal-tex-arg (number-to-string (calendar-extract-day date)))
1228 (cal-tex-arg (cal-tex-latexify-list diary-list date)) 1213 (cal-tex-arg (cal-tex-latexify-list diary-list date))
1229 (cal-tex-arg (cal-tex-latexify-list holidays date)) 1214 (cal-tex-arg (cal-tex-latexify-list holidays date))
1230 (cal-tex-arg (eval cal-tex-daily-string)) 1215 (cal-tex-arg (eval cal-tex-daily-string t))
1231 (insert "%\n") 1216 (insert "%\n")
1232 (setq date (cal-tex-incr-date date))) 1217 (setq date (cal-tex-incr-date date)))
1233 (dotimes (_jdummy 2) 1218 (dotimes (_jdummy 2)
@@ -1236,7 +1221,7 @@ shown are hard-coded to 8-12, 13-17."
1236 (cal-tex-arg (number-to-string (calendar-extract-day date))) 1221 (cal-tex-arg (number-to-string (calendar-extract-day date)))
1237 (cal-tex-arg (cal-tex-latexify-list diary-list date)) 1222 (cal-tex-arg (cal-tex-latexify-list diary-list date))
1238 (cal-tex-arg (cal-tex-latexify-list holidays date)) 1223 (cal-tex-arg (cal-tex-latexify-list holidays date))
1239 (cal-tex-arg (eval cal-tex-daily-string)) 1224 (cal-tex-arg (eval cal-tex-daily-string t))
1240 (insert "%\n") 1225 (insert "%\n")
1241 (setq date (cal-tex-incr-date date))) 1226 (setq date (cal-tex-incr-date date)))
1242 (unless (= i (1- n)) 1227 (unless (= i (1- n))
diff --git a/lisp/calendar/calendar.el b/lisp/calendar/calendar.el
index 42fc210c1e1..2e90d6e4639 100644
--- a/lisp/calendar/calendar.el
+++ b/lisp/calendar/calendar.el
@@ -871,7 +871,15 @@ current word of the diary entry, so in no case can the pattern match more than
871a portion of the first word of the diary entry. 871a portion of the first word of the diary entry.
872 872
873For examples of three common styles, see `diary-american-date-forms', 873For examples of three common styles, see `diary-american-date-forms',
874`diary-european-date-forms', and `diary-iso-date-forms'." 874`diary-european-date-forms', and `diary-iso-date-forms'.
875
876If you customize this variable, you should also customize the variable
877`diary-date-insertion-form' to contain a pseudo-pattern which produces
878dates that match one of the forms in this variable. (If
879`diary-date-insertion-form' does not correspond to one of the patterns
880in this variable, then the diary will not recognize such dates,
881including those inserted into the diary from the calendar with
882`diary-insert-entry'.)"
875 :type '(repeat (choice (cons :tag "Backup" 883 :type '(repeat (choice (cons :tag "Backup"
876 :value (backup . nil) 884 :value (backup . nil)
877 (const backup) 885 (const backup)
@@ -895,6 +903,50 @@ For examples of three common styles, see `diary-american-date-forms',
895 (diary)))) 903 (diary))))
896 :group 'diary) 904 :group 'diary)
897 905
906(defconst diary-american-date-insertion-form '(month "/" day "/" year)
907 "Pseudo-pattern for American dates in `diary-date-insertion-form'")
908
909(defconst diary-european-date-insertion-form '(day "/" month "/" year)
910 "Pseudo-pattern for European dates in `diary-date-insertion-form'")
911
912(defconst diary-iso-date-insertion-form '(year "/" month "/" day)
913 "Pseudo-pattern for ISO dates in `diary-date-insertion-form'")
914
915(defcustom diary-date-insertion-form
916 (cond ((eq calendar-date-style 'iso) diary-iso-date-insertion-form)
917 ((eq calendar-date-style 'european) diary-european-date-insertion-form)
918 (t diary-american-date-insertion-form))
919 "Pseudo-pattern describing how to format a date for a new diary entry.
920
921A pseudo-pattern is a list of expressions that can include the symbols
922`month', `day', and `year' (all numbers in string form), and `monthname'
923and `dayname' (both alphabetic strings). For example, a typical American
924form would be
925
926 (month \"/\" day \"/\" (substring year -2))
927
928whereas
929
930 ((format \"%9s, %9s %2s, %4s\" dayname monthname day year))
931
932would give the usual American style in fixed-length fields.
933
934This pattern will be used by `calendar-date-string' (which see) to
935format dates when inserting them with `diary-insert-entry', or when
936importing them from other formats into the diary.
937
938If you customize this variable, you should also customize the variable
939`diary-date-forms' to include a pseudo-pattern which matches dates
940produced by this pattern. (If there is no corresponding pattern in
941`diary-date-forms', then the diary will not recognize such dates,
942including those inserted into the diary from the calendar with
943`diary-insert-entry'.)"
944 :version "31.1"
945 :type 'sexp
946 :risky t
947 :set-after '(calendar-date-style)
948 :group 'diary)
949
898;; Next three are provided to aid in setting calendar-date-display-form. 950;; Next three are provided to aid in setting calendar-date-display-form.
899(defcustom calendar-iso-date-display-form '((format "%s-%.2d-%.2d" year 951(defcustom calendar-iso-date-display-form '((format "%s-%.2d-%.2d" year
900 (string-to-number month) 952 (string-to-number month)
@@ -1028,7 +1080,9 @@ The valid styles are described in the documentation of `calendar-date-style'."
1028 calendar-month-header 1080 calendar-month-header
1029 (symbol-value (intern-soft (format "calendar-%s-month-header" style))) 1081 (symbol-value (intern-soft (format "calendar-%s-month-header" style)))
1030 diary-date-forms 1082 diary-date-forms
1031 (symbol-value (intern-soft (format "diary-%s-date-forms" style)))) 1083 (symbol-value (intern-soft (format "diary-%s-date-forms" style)))
1084 diary-date-insertion-form
1085 (symbol-value (intern-soft (format "diary-%s-date-insertion-form" style))))
1032 (calendar-redraw)) 1086 (calendar-redraw))
1033 1087
1034(defcustom diary-show-holidays-flag t 1088(defcustom diary-show-holidays-flag t
@@ -1297,6 +1351,16 @@ return negative results."
1297 (/ offset-years 400) 1351 (/ offset-years 400)
1298 (calendar-day-number '(12 31 -1))))))) ; days in year 1 BC 1352 (calendar-day-number '(12 31 -1))))))) ; days in year 1 BC
1299 1353
1354;; This function is the inverse of `calendar-day-number':
1355(defun calendar-date-from-day-of-year (year dayno)
1356 "Return the date of the DAYNO-th day in YEAR.
1357DAYNO must be an integer between -366 and 366."
1358 (calendar-gregorian-from-absolute
1359 (+ (if (< dayno 0)
1360 (+ 1 dayno (if (calendar-leap-year-p year) 366 365))
1361 dayno)
1362 (calendar-absolute-from-gregorian (list 12 31 (1- year))))))
1363
1300;;;###autoload 1364;;;###autoload
1301(defun calendar (&optional arg) 1365(defun calendar (&optional arg)
1302 "Display a three-month Gregorian calendar. 1366 "Display a three-month Gregorian calendar.
@@ -1598,143 +1662,143 @@ Otherwise, use the selected window of EVENT's frame."
1598 mark-defun mark-whole-buffer mark-page 1662 mark-defun mark-whole-buffer mark-page
1599 downcase-region upcase-region kill-region 1663 downcase-region upcase-region kill-region
1600 copy-region-as-kill capitalize-region write-region)) 1664 copy-region-as-kill capitalize-region write-region))
1601 (define-key map (vector 'remap c) 'calendar-not-implemented)) 1665 (define-key map (vector 'remap c) #'calendar-not-implemented))
1602 (define-key map "<" 'calendar-scroll-right) 1666 (define-key map "<" #'calendar-scroll-right)
1603 (define-key map "\C-x<" 'calendar-scroll-right) 1667 (define-key map "\C-x<" #'calendar-scroll-right)
1604 (define-key map [S-wheel-up] 'calendar-scroll-right) 1668 (define-key map [S-wheel-up] #'calendar-scroll-right)
1605 (define-key map [prior] 'calendar-scroll-right-three-months) 1669 (define-key map [prior] #'calendar-scroll-right-three-months)
1606 (define-key map "\ev" 'calendar-scroll-right-three-months) 1670 (define-key map "\ev" #'calendar-scroll-right-three-months)
1607 (define-key map [wheel-up] 'calendar-scroll-right-three-months) 1671 (define-key map [wheel-up] #'calendar-scroll-right-three-months)
1608 (define-key map [M-wheel-up] 'calendar-backward-year) 1672 (define-key map [M-wheel-up] #'calendar-backward-year)
1609 (define-key map ">" 'calendar-scroll-left) 1673 (define-key map ">" #'calendar-scroll-left)
1610 (define-key map "\C-x>" 'calendar-scroll-left) 1674 (define-key map "\C-x>" #'calendar-scroll-left)
1611 (define-key map [S-wheel-down] 'calendar-scroll-left) 1675 (define-key map [S-wheel-down] #'calendar-scroll-left)
1612 (define-key map [next] 'calendar-scroll-left-three-months) 1676 (define-key map [next] #'calendar-scroll-left-three-months)
1613 (define-key map "\C-v" 'calendar-scroll-left-three-months) 1677 (define-key map "\C-v" #'calendar-scroll-left-three-months)
1614 (define-key map [wheel-down] 'calendar-scroll-left-three-months) 1678 (define-key map [wheel-down] #'calendar-scroll-left-three-months)
1615 (define-key map [M-wheel-down] 'calendar-forward-year) 1679 (define-key map [M-wheel-down] #'calendar-forward-year)
1616 (define-key map "\C-l" 'calendar-recenter) 1680 (define-key map "\C-l" #'calendar-recenter)
1617 (define-key map "\C-b" 'calendar-backward-day) 1681 (define-key map "\C-b" #'calendar-backward-day)
1618 (define-key map "\C-p" 'calendar-backward-week) 1682 (define-key map "\C-p" #'calendar-backward-week)
1619 (define-key map "\e{" 'calendar-backward-month) 1683 (define-key map "\e{" #'calendar-backward-month)
1620 (define-key map "{" 'calendar-backward-month) 1684 (define-key map "{" #'calendar-backward-month)
1621 (define-key map "\C-x[" 'calendar-backward-year) 1685 (define-key map "\C-x[" #'calendar-backward-year)
1622 (define-key map "[" 'calendar-backward-year) 1686 (define-key map "[" #'calendar-backward-year)
1623 (define-key map "\C-f" 'calendar-forward-day) 1687 (define-key map "\C-f" #'calendar-forward-day)
1624 (define-key map "\C-n" 'calendar-forward-week) 1688 (define-key map "\C-n" #'calendar-forward-week)
1625 (define-key map [left] 'calendar-backward-day) 1689 (define-key map [left] #'calendar-backward-day)
1626 (define-key map [up] 'calendar-backward-week) 1690 (define-key map [up] #'calendar-backward-week)
1627 (define-key map [right] 'calendar-forward-day) 1691 (define-key map [right] #'calendar-forward-day)
1628 (define-key map [down] 'calendar-forward-week) 1692 (define-key map [down] #'calendar-forward-week)
1629 (define-key map "\e}" 'calendar-forward-month) 1693 (define-key map "\e}" #'calendar-forward-month)
1630 (define-key map "}" 'calendar-forward-month) 1694 (define-key map "}" #'calendar-forward-month)
1631 (define-key map "\C-x]" 'calendar-forward-year) 1695 (define-key map "\C-x]" #'calendar-forward-year)
1632 (define-key map "]" 'calendar-forward-year) 1696 (define-key map "]" #'calendar-forward-year)
1633 (define-key map "\C-a" 'calendar-beginning-of-week) 1697 (define-key map "\C-a" #'calendar-beginning-of-week)
1634 (define-key map "\C-e" 'calendar-end-of-week) 1698 (define-key map "\C-e" #'calendar-end-of-week)
1635 (define-key map "\ea" 'calendar-beginning-of-month) 1699 (define-key map "\ea" #'calendar-beginning-of-month)
1636 (define-key map "\ee" 'calendar-end-of-month) 1700 (define-key map "\ee" #'calendar-end-of-month)
1637 (define-key map "\e<" 'calendar-beginning-of-year) 1701 (define-key map "\e<" #'calendar-beginning-of-year)
1638 (define-key map "\e>" 'calendar-end-of-year) 1702 (define-key map "\e>" #'calendar-end-of-year)
1639 (define-key map "\C-@" 'calendar-set-mark) 1703 (define-key map "\C-@" #'calendar-set-mark)
1640 ;; Many people are used to typing C-SPC and getting C-@. 1704 ;; Many people are used to typing C-SPC and getting C-@.
1641 (define-key map [?\C-\s] 'calendar-set-mark) 1705 (define-key map [?\C-\s] #'calendar-set-mark)
1642 (define-key map "\C-x\C-x" 'calendar-exchange-point-and-mark) 1706 (define-key map "\C-x\C-x" #'calendar-exchange-point-and-mark)
1643 (define-key map "\e=" 'calendar-count-days-region) 1707 (define-key map "\e=" #'calendar-count-days-region)
1644 (define-key map "gd" 'calendar-goto-date) 1708 (define-key map "gd" #'calendar-goto-date)
1645 (define-key map "gD" 'calendar-goto-day-of-year) 1709 (define-key map "gD" #'calendar-goto-day-of-year)
1646 (define-key map "gj" 'calendar-julian-goto-date) 1710 (define-key map "gj" #'calendar-julian-goto-date)
1647 (define-key map "ga" 'calendar-astro-goto-day-number) 1711 (define-key map "ga" #'calendar-astro-goto-day-number)
1648 (define-key map "gh" 'calendar-hebrew-goto-date) 1712 (define-key map "gh" #'calendar-hebrew-goto-date)
1649 (define-key map "gi" 'calendar-islamic-goto-date) 1713 (define-key map "gi" #'calendar-islamic-goto-date)
1650 (define-key map "gb" 'calendar-bahai-goto-date) 1714 (define-key map "gb" #'calendar-bahai-goto-date)
1651 (define-key map "gC" 'calendar-chinese-goto-date) 1715 (define-key map "gC" #'calendar-chinese-goto-date)
1652 (define-key map "gk" 'calendar-coptic-goto-date) 1716 (define-key map "gk" #'calendar-coptic-goto-date)
1653 (define-key map "ge" 'calendar-ethiopic-goto-date) 1717 (define-key map "ge" #'calendar-ethiopic-goto-date)
1654 (define-key map "gp" 'calendar-persian-goto-date) 1718 (define-key map "gp" #'calendar-persian-goto-date)
1655 (define-key map "gc" 'calendar-iso-goto-date) 1719 (define-key map "gc" #'calendar-iso-goto-date)
1656 (define-key map "gw" 'calendar-iso-goto-week) 1720 (define-key map "gw" #'calendar-iso-goto-week)
1657 (define-key map "gf" 'calendar-french-goto-date) 1721 (define-key map "gf" #'calendar-french-goto-date)
1658 (define-key map "gml" 'calendar-mayan-goto-long-count-date) 1722 (define-key map "gml" #'calendar-mayan-goto-long-count-date)
1659 (define-key map "gmpc" 'calendar-mayan-previous-round-date) 1723 (define-key map "gmpc" #'calendar-mayan-previous-round-date)
1660 (define-key map "gmnc" 'calendar-mayan-next-round-date) 1724 (define-key map "gmnc" #'calendar-mayan-next-round-date)
1661 (define-key map "gmph" 'calendar-mayan-previous-haab-date) 1725 (define-key map "gmph" #'calendar-mayan-previous-haab-date)
1662 (define-key map "gmnh" 'calendar-mayan-next-haab-date) 1726 (define-key map "gmnh" #'calendar-mayan-next-haab-date)
1663 (define-key map "gmpt" 'calendar-mayan-previous-tzolkin-date) 1727 (define-key map "gmpt" #'calendar-mayan-previous-tzolkin-date)
1664 (define-key map "gmnt" 'calendar-mayan-next-tzolkin-date) 1728 (define-key map "gmnt" #'calendar-mayan-next-tzolkin-date)
1665 (define-key map "Aa" 'appt-add) 1729 (define-key map "Aa" #'appt-add)
1666 (define-key map "Ad" 'appt-delete) 1730 (define-key map "Ad" 'appt-delete)
1667 (define-key map "S" 'calendar-sunrise-sunset) 1731 (define-key map "S" #'calendar-sunrise-sunset)
1668 (define-key map "M" 'calendar-lunar-phases) 1732 (define-key map "M" #'calendar-lunar-phases)
1669 (define-key map " " 'scroll-other-window) 1733 (define-key map " " #'scroll-other-window)
1670 (define-key map [?\S-\ ] 'scroll-other-window-down) 1734 (define-key map [?\S-\ ] #'scroll-other-window-down)
1671 (define-key map "\d" 'scroll-other-window-down) 1735 (define-key map "\d" #'scroll-other-window-down)
1672 (define-key map "\C-c\C-l" 'calendar-redraw) 1736 (define-key map "\C-c\C-l" #'calendar-redraw)
1673 (define-key map "." 'calendar-goto-today) 1737 (define-key map "." #'calendar-goto-today)
1674 (define-key map "o" 'calendar-other-month) 1738 (define-key map "o" #'calendar-other-month)
1675 (define-key map "q" 'calendar-exit) 1739 (define-key map "q" #'calendar-exit)
1676 (define-key map "a" 'calendar-list-holidays) 1740 (define-key map "a" #'calendar-list-holidays)
1677 (define-key map "h" 'calendar-cursor-holidays) 1741 (define-key map "h" #'calendar-cursor-holidays)
1678 (define-key map "x" 'calendar-mark-holidays) 1742 (define-key map "x" #'calendar-mark-holidays)
1679 (define-key map "u" 'calendar-unmark) 1743 (define-key map "u" #'calendar-unmark)
1680 (define-key map "m" 'diary-mark-entries) 1744 (define-key map "m" #'diary-mark-entries)
1681 (define-key map "d" 'diary-view-entries) 1745 (define-key map "d" #'diary-view-entries)
1682 (define-key map "D" 'diary-view-other-diary-entries) 1746 (define-key map "D" #'diary-view-other-diary-entries)
1683 (define-key map "s" 'diary-show-all-entries) 1747 (define-key map "s" #'diary-show-all-entries)
1684 (define-key map "pd" 'calendar-print-day-of-year) 1748 (define-key map "pd" #'calendar-print-day-of-year)
1685 (define-key map "pC" 'calendar-chinese-print-date) 1749 (define-key map "pC" #'calendar-chinese-print-date)
1686 (define-key map "pk" 'calendar-coptic-print-date) 1750 (define-key map "pk" #'calendar-coptic-print-date)
1687 (define-key map "pe" 'calendar-ethiopic-print-date) 1751 (define-key map "pe" #'calendar-ethiopic-print-date)
1688 (define-key map "pp" 'calendar-persian-print-date) 1752 (define-key map "pp" #'calendar-persian-print-date)
1689 (define-key map "pc" 'calendar-iso-print-date) 1753 (define-key map "pc" #'calendar-iso-print-date)
1690 (define-key map "pj" 'calendar-julian-print-date) 1754 (define-key map "pj" #'calendar-julian-print-date)
1691 (define-key map "pa" 'calendar-astro-print-day-number) 1755 (define-key map "pa" #'calendar-astro-print-day-number)
1692 (define-key map "ph" 'calendar-hebrew-print-date) 1756 (define-key map "ph" #'calendar-hebrew-print-date)
1693 (define-key map "pi" 'calendar-islamic-print-date) 1757 (define-key map "pi" #'calendar-islamic-print-date)
1694 (define-key map "pb" 'calendar-bahai-print-date) 1758 (define-key map "pb" #'calendar-bahai-print-date)
1695 (define-key map "pf" 'calendar-french-print-date) 1759 (define-key map "pf" #'calendar-french-print-date)
1696 (define-key map "pm" 'calendar-mayan-print-date) 1760 (define-key map "pm" #'calendar-mayan-print-date)
1697 (define-key map "po" 'calendar-print-other-dates) 1761 (define-key map "po" #'calendar-print-other-dates)
1698 (define-key map "id" 'diary-insert-entry) 1762 (define-key map "id" #'diary-insert-entry)
1699 (define-key map "iw" 'diary-insert-weekly-entry) 1763 (define-key map "iw" #'diary-insert-weekly-entry)
1700 (define-key map "im" 'diary-insert-monthly-entry) 1764 (define-key map "im" #'diary-insert-monthly-entry)
1701 (define-key map "iy" 'diary-insert-yearly-entry) 1765 (define-key map "iy" #'diary-insert-yearly-entry)
1702 (define-key map "ia" 'diary-insert-anniversary-entry) 1766 (define-key map "ia" #'diary-insert-anniversary-entry)
1703 (define-key map "ib" 'diary-insert-block-entry) 1767 (define-key map "ib" #'diary-insert-block-entry)
1704 (define-key map "ic" 'diary-insert-cyclic-entry) 1768 (define-key map "ic" #'diary-insert-cyclic-entry)
1705 (define-key map "ihd" 'diary-hebrew-insert-entry) 1769 (define-key map "ihd" #'diary-hebrew-insert-entry)
1706 (define-key map "ihm" 'diary-hebrew-insert-monthly-entry) 1770 (define-key map "ihm" #'diary-hebrew-insert-monthly-entry)
1707 (define-key map "ihy" 'diary-hebrew-insert-yearly-entry) 1771 (define-key map "ihy" #'diary-hebrew-insert-yearly-entry)
1708 (define-key map "iid" 'diary-islamic-insert-entry) 1772 (define-key map "iid" #'diary-islamic-insert-entry)
1709 (define-key map "iim" 'diary-islamic-insert-monthly-entry) 1773 (define-key map "iim" #'diary-islamic-insert-monthly-entry)
1710 (define-key map "iiy" 'diary-islamic-insert-yearly-entry) 1774 (define-key map "iiy" #'diary-islamic-insert-yearly-entry)
1711 (define-key map "iBd" 'diary-bahai-insert-entry) 1775 (define-key map "iBd" #'diary-bahai-insert-entry)
1712 (define-key map "iBm" 'diary-bahai-insert-monthly-entry) 1776 (define-key map "iBm" #'diary-bahai-insert-monthly-entry)
1713 (define-key map "iBy" 'diary-bahai-insert-yearly-entry) 1777 (define-key map "iBy" #'diary-bahai-insert-yearly-entry)
1714 (define-key map "iCd" 'diary-chinese-insert-entry) 1778 (define-key map "iCd" #'diary-chinese-insert-entry)
1715 (define-key map "iCm" 'diary-chinese-insert-monthly-entry) 1779 (define-key map "iCm" #'diary-chinese-insert-monthly-entry)
1716 (define-key map "iCy" 'diary-chinese-insert-yearly-entry) 1780 (define-key map "iCy" #'diary-chinese-insert-yearly-entry)
1717 (define-key map "iCa" 'diary-chinese-insert-anniversary-entry) 1781 (define-key map "iCa" #'diary-chinese-insert-anniversary-entry)
1718 (define-key map "?" 'calendar-goto-info-node) 1782 (define-key map "?" #'calendar-goto-info-node)
1719 (define-key map "Hm" 'cal-html-cursor-month) 1783 (define-key map "Hm" #'cal-html-cursor-month)
1720 (define-key map "Hy" 'cal-html-cursor-year) 1784 (define-key map "Hy" #'cal-html-cursor-year)
1721 (define-key map "tm" 'cal-tex-cursor-month) 1785 (define-key map "tm" #'cal-tex-cursor-month)
1722 (define-key map "tM" 'cal-tex-cursor-month-landscape) 1786 (define-key map "tM" #'cal-tex-cursor-month-landscape)
1723 (define-key map "td" 'cal-tex-cursor-day) 1787 (define-key map "td" #'cal-tex-cursor-day)
1724 (define-key map "tw1" 'cal-tex-cursor-week) 1788 (define-key map "tw1" #'cal-tex-cursor-week)
1725 (define-key map "tw2" 'cal-tex-cursor-week2) 1789 (define-key map "tw2" #'cal-tex-cursor-week2)
1726 (define-key map "tw3" 'cal-tex-cursor-week-iso) ; FIXME twi ? 1790 (define-key map "tw3" #'cal-tex-cursor-week-iso) ; FIXME twi ?
1727 (define-key map "tw4" 'cal-tex-cursor-week-monday) ; twm ? 1791 (define-key map "tw4" #'cal-tex-cursor-week-monday) ; twm ?
1728 (define-key map "twW" 'cal-tex-cursor-week2-summary) 1792 (define-key map "twW" #'cal-tex-cursor-week2-summary)
1729 (define-key map "tfd" 'cal-tex-cursor-filofax-daily) 1793 (define-key map "tfd" #'cal-tex-cursor-filofax-daily)
1730 (define-key map "tfw" 'cal-tex-cursor-filofax-2week) 1794 (define-key map "tfw" #'cal-tex-cursor-filofax-2week)
1731 (define-key map "tfW" 'cal-tex-cursor-filofax-week) 1795 (define-key map "tfW" #'cal-tex-cursor-filofax-week)
1732 (define-key map "tfy" 'cal-tex-cursor-filofax-year) 1796 (define-key map "tfy" #'cal-tex-cursor-filofax-year)
1733 (define-key map "ty" 'cal-tex-cursor-year) 1797 (define-key map "ty" #'cal-tex-cursor-year)
1734 (define-key map "tY" 'cal-tex-cursor-year-landscape) 1798 (define-key map "tY" #'cal-tex-cursor-year-landscape)
1735 1799
1736 (define-key map [menu-bar edit] 'undefined) 1800 (define-key map [menu-bar edit] #'undefined)
1737 (define-key map [menu-bar search] 'undefined) 1801 (define-key map [menu-bar search] #'undefined)
1738 1802
1739 (easy-menu-define nil map nil cal-menu-sunmoon-menu) 1803 (easy-menu-define nil map nil cal-menu-sunmoon-menu)
1740 (easy-menu-define nil map nil cal-menu-diary-menu) 1804 (easy-menu-define nil map nil cal-menu-diary-menu)
diff --git a/lisp/calendar/diary-icalendar.el b/lisp/calendar/diary-icalendar.el
new file mode 100644
index 00000000000..ed8706c50a7
--- /dev/null
+++ b/lisp/calendar/diary-icalendar.el
@@ -0,0 +1,3730 @@
1;;; diary-icalendar.el --- Display iCalendar data in diary -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2025-2026 Free Software Foundation, Inc.
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: January 2025
7;; Keywords: calendar
8;; Human-Keywords: diary, calendar, iCalendar
9
10;; This file is part of GNU Emacs.
11
12;; This file is free software: you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; This file is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with this file. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; This file is a replacement for icalendar.el that uses a new parser
28;; and offers more features.
29
30;;; Code:
31
32(eval-when-compile (require 'cl-lib))
33(eval-when-compile (require 'icalendar-macs))
34(require 'icalendar)
35(require 'icalendar-parser)
36(require 'icalendar-utils)
37(require 'icalendar-recur)
38(require 'icalendar-ast)
39(require 'calendar)
40(require 'cal-dst)
41(require 'diary-lib)
42(require 'skeleton)
43(require 'seq)
44(require 'rx)
45(require 'pp)
46
47;; Customization
48(defgroup diary-icalendar nil
49 "iCalendar import, export, and display in diary."
50 :version "31.1"
51 :group 'diary
52 :prefix 'diary-icalendar)
53
54;;; Import customizations
55(defgroup diary-icalendar-import nil
56 "iCalendar import into diary."
57 :version "31.1"
58 :group 'diary-icalendar
59 :prefix 'diary-icalendar)
60
61(defcustom di:always-import-quietly nil
62 "When non-nil, diary will never ask for confirmations when importing events.
63
64`diary-icalendar-import-file' and `diary-icalendar-import-buffer' both
65accept an optional argument, QUIETLY, which determines whether these
66functions ask for confirmation when importing individual events and
67saving the diary file. If you set this variable to t, you will never be
68asked to confirm."
69 :version "31.1"
70 :type '(choice (const :tag "Ask for confirmations" nil)
71 (const :tag "Never ask for confirmations" t)))
72
73(defcustom di:after-mailcap-viewer-hook nil
74 "Hook run after `diary-icalendar-mailcap-viewer'.
75
76The functions in this hook will be run in a temporary buffer after
77formatting the contents of iCalendar data as diary entries in that
78buffer. You can add functions to this hook if you want, for example, to
79copy these entries somewhere else."
80 :version "31.1"
81 :type '(hook))
82
83(defcustom di:attachment-directory nil
84 "Directory in which to save iCalendar attachments when importing.
85
86If the value is nil, binary attachments encoded in an ATTACH property
87are never saved. If it is the name of a directory, attachments will be
88saved in per-component subdirectories of this directory, with each
89subdirectory named by the component's UID value."
90 :version "31.1"
91 :type '(choice
92 (const :tag "Do not save attachments" nil)
93 directory))
94
95(defcustom di:time-format "%H:%M"
96 "Format string to use for event times.
97
98The value must be a valid format string for `format-time-string'; see
99its docstring for more information. The value only needs to format clock
100times, and should format them in a way that will be recognized by
101`diary-time-regexp'. (Date information is formatted separately at the
102start of the imported entry.) Examples:
103
104 \"%H:%M\" - 24-hour, 0-padded: 09:00 or 21:00
105 \"%k.%Mh\" - 24-hour, blank-padded: 9.00h or 21.00h
106 \"%I:%M%p\" - 12-hour, 0-padded, with AM/PM: 09:00AM or 09:00PM
107 \"%l.%M%p\" - 12-hour, blank-padded, with AM/PM: 9.00AM or 9.00PM"
108 :version "31.1"
109 :type '(string))
110
111(defcustom di:attendee-format-function #'di:attendee-skeleton
112 "Function to format ATTENDEE properties during diary import.
113
114This should be a function which inserts information about an
115`icalendar-attendee' into the current buffer. It is convenient to
116express such a function as a skeleton; see `define-skeleton' and
117`skeleton-insert' for more information.
118
119The function will be called with one argument, ATTENDEE, which will be
120an `icalendar-attendee' syntax node. It should insert information about
121the attendee into the current buffer. See `icalendar-with-property' for
122a convenient way to bind the data in ATTENDEE.
123
124For convenience when writing this function as a skeleton, the following
125variables will also be (dynamically) bound when the function is called.
126All values will be strings (unless another type is noted), or nil:
127
128`attendee-address' - address, with \"mailto:\" removed
129`attendee-cn' - common name (`icalendar-cnparam')
130`attendee-cutype' - calendar user type (`icalendar-cutypeparam')
131`attendee-role' - role in the event (`icalendar-roleparam')
132`attendee-partstat' - participation status (`icalendar-partstatparam')
133`attendee-rsvp' - whether an RSVP is requested (`icalendar-rsvpparam')"
134 :version "31.1"
135 :type '(radio (function-item di:attendee-skeleton)
136 (function :tag "Other function")))
137
138(defcustom di:skip-addresses-regexp
139 (concat "\\<" (regexp-quote user-mail-address) "\\'")
140 "Regular expression matching addresses to skip when importing.
141
142This regular expression should match calendar addresses (which are
143typically \"mailto:\" URIs) which should be skipped when importing
144ATTENDEE, ORGANIZER, and other iCalendar properties that identify a
145contact.
146
147You can make this match your own email address(es) to prevent them from
148being formatted by `diary-icalendar-attendee-format-function' and
149listed in diary entries."
150 :version "31.1"
151 :type '(regexp))
152
153(defcustom di:vevent-format-function #'di:vevent-skeleton
154 "Function to format VEVENT components for the diary.
155
156This function is called with one argument VEVENT, an `icalendar-vevent'.
157It should insert formatted data from this event into the current buffer.
158It is convenient to express such a function as a skeleton; see
159`define-skeleton' and `skeleton-insert' for more information. See
160`icalendar-with-component' for a convenient way to bind the data in
161VEVENT.
162
163For convenience when writing this function as a skeleton, the following
164variables will be (dynamically) bound when the function is called. All
165values will be strings unless another type is noted, or nil:
166
167`ical-as-alarm' (symbol) - non-nil when the event should be formatted for an
168 alarm notification in advance of the event. The symbol indicates the
169 type of alarm: `email' means to format the event as the body of an email.
170 (Currently only used for EMAIL alarms; see `diary-icalendar-export-alarms'.)
171`ical-attachments' (list of strings) - URLs or filenames of attachments
172 in the event
173`ical-attendees' (list of strings) - the participants of the event,
174 formatted by `diary-icalendar-attendee-format-function'
175`ical-categories' (list of strings) - categories specified in the event
176`ical-access' - the event's access classification
177`ical-comments' (list of strings) - comments specified in the event
178`ical-description' - the event's description
179`ical-start' - start date and time in a single string. When importing,
180 includes the date, otherwise just the (local) time.
181`ical-end' - end date and time in a single string. When importing,
182 includes the date, otherwise just the (local) time.
183`ical-start-to-end' - a single string containing both start and end date and
184 (local) time. If the event starts and ends on the same day, the date
185 is not repeated. When importing, dates are included, and the string
186 may contain a diary s-exp; when displaying, the string contains only
187 the times for the displayed date. If there is no end date, same as
188 `ical-start'.
189`ical-importing' (a boolean) - t if the event should be formatted for import.
190 When nil, the event should be formatted for display rather than import.
191 When importing it is important to include all information from the event
192 that you want to be saved in the diary; when displaying, information like
193 the date (or date-related S-expressions) and UID can be left out.
194`ical-location' - the event's location, or geographical coordinates
195`ical-nonmarking' (a boolean) - if non-nil, the diary entry should be nonmarking
196`ical-organizer' - the event's organizer, formatted by
197 `diary-icalendar-attendee-format-function'
198`ical-priority' (a number) - the event's priority (1 = highest priority,
199 9 = lowest; 0 = undefined)
200`ical-rrule-sexp' - a string containing a diary S-expression for a
201 recurring event. If this is non-nil, you should normally use it
202 instead of the start-* and end-* variables to form the date of the
203 entry.
204`ical-status' - overall status specified by the organizer (e.g. \"confirmed\")
205`ical-summary' - a summary of the event
206`ical-transparency' - the event's time transparency status, either
207 \"OPAQUE\" (busy) or \"TRANSPARENT\" (free); see `icalendar-transp'
208`ical-uid' - the unique identifier of the event
209`ical-url' - a URL for the event"
210 :version "31.1"
211 :type '(radio (function-item di:vevent-skeleton)
212 (function :tag "Other function")))
213
214(defcustom di:vjournal-format-function #'di:vjournal-skeleton
215 "Function to format VJOURNAL components for the diary.
216
217This function is called with one argument VJOURNAL, an
218`icalendar-vjournal'. It should insert formatted data from this journal
219entry into the current buffer. It is convenient to express such a
220function as a skeleton; see `define-skeleton' and `skeleton-insert' for
221more information, and see `diary-icalendar-vjournal-skeleton' for an
222example. See `icalendar-with-component' for a convenient way to bind
223the data in VJOURNAL.
224
225For convenience when writing this function as a skeleton, the following
226variables will be (dynamically) bound when the function is called. All
227values will be strings unless another type is noted, or nil:
228
229`ical-attachments' (list of strings) - URLs or filenames of attachments
230 in the journal entry
231`ical-attendees' (list of strings) - the participants of the journal entry,
232 formatted by `diary-icalendar-attendee-format-function'
233`ical-categories' (list of strings) - categories specified in the journal entry
234`ical-access' - the journal entry's access classification
235`ical-comments' (list of strings) - comments specified in the journal entry
236`ical-description' - the journal entry's description(s) as a single
237 string, separated by newlines (more than one description is allowed in
238 VJOURNAL components)
239`ical-start' - start date and time in a single string. When importing,
240 includes the date, otherwise just the (local) time.
241`ical-importing' (a boolean) - t if the journal entry should be
242 formatted for import. When nil, the entry should be formatted for
243 display rather than import. When importing it is important to include
244 all information from the entry that you want to be saved in the diary;
245 when displaying, information like the date (or date-related
246 S-expressions) and UID can be left out.
247`ical-nonmarking' (a boolean) - if non-nil, the diary entry should be nonmarking
248`ical-organizer' - the journal entry's organizer, formatted by
249 `diary-icalendar-attendee-format-function'
250`ical-rrule-sexp' - a string containing a diary S-expression for a recurring
251 journal entry. If this is non-nil, you should normally use it instead
252 of the start-* variables to form the date of the entry.
253`ical-status' - overall status specified by the organizer (e.g. \"draft\")
254`ical-summary' - a summary of the journal entry
255`ical-uid' - the unique identifier of the journal entry
256`ical-url' - a URL for the journal entry"
257 :version "31.1"
258 :type '(radio (function-item di:vjournal-skeleton)
259 (function :tag "Other function")))
260
261(defcustom di:import-vjournal-as-nonmarking t
262 "Whether to import VJOURNAL components as nonmarking diary entries.
263
264If this variable is non-nil, VJOURNAL components will be imported into
265the diary as \"nonmarking\" entries by prefixing
266`diary-nonmarking-symbol'. This means they will not cause their date to
267be marked in the calendar when the command `diary-mark-entries' is
268called. See Info node `(emacs)Displaying the Diary' for more
269information."
270 :version "31.1"
271 :type '(choice (const :tag "Import as nonmarking entries" t)
272 (const :tag "Import as normal (marking) entries" nil)))
273
274(defcustom di:vtodo-format-function #'di:vtodo-skeleton
275 "Function to format VTODO components for the diary.
276
277This function is called with one argument VTODO, an `icalendar-vtodo'.
278It should insert formatted data from this task into the current buffer.
279It is convenient to express such a function as a skeleton; see
280`define-skeleton' and `skeleton-insert' for more information. See
281`icalendar-with-component' for a convenient way to bind the data in
282VTODO.
283
284For convenience when writing this function as a skeleton, the following
285variables will be (dynamically) bound when the function is called. All
286values will be strings unless another type is noted, or nil:
287
288`ical-as-alarm' (symbol) - non-nil when the task should be formatted for
289 an alarm notification in advance of the task. The symbol indicates
290 the type of alarm: `email' means to format the task as the body of an
291 email. (Currently only used for EMAIL alarms; see
292 `diary-icalendar-export-alarms'.)
293`ical-attachments' (list of strings) - URLs or filenames of attachments
294 in the task
295`ical-attendees' (list of strings) - the participants of the task,
296 formatted by `diary-icalendar-attendee-format-function'
297`ical-categories' (list of strings) - categories specified in the task
298`ical-access' - the task's access classification
299`ical-comments' (list of strings) - comments specified in the task
300`ical-completed' - when the task was completed, formatted as a local
301 date-time string
302`ical-description' - the task's description
303`ical-start' - start-date and time in a single string. When importing,
304 includes the date, otherwise just the (local) time
305`ical-start-to-end' - a single string containing both start and due date
306 and time. If the task starts and ends on the same day, the date is
307 not repeated. When importing, dates are included, and the string may
308 contain a diary s-exp; when displaying, the string contains only the
309 times for the displayed date. If there is no end date, same as
310 `ical-start'.
311`ical-due' - due date and time in a single string
312`ical-end' - same as `ical-due'
313`ical-work-time-sexp' - when the task has both a start date and a due date,
314 this is a %%(diary-time-block ...) diary S-expression representing the
315 time from the start date to the due date (only non-nil when
316 importing). You can use this e.g. to make a separate entry for the
317 task's work time, so that it shows up every day in the diary until it
318 is due.
319`ical-importing' (a boolean) - t if the task should be formatted for import.
320 When nil, the task should be formatted for display rather than import.
321 When importing it is important to include all information from the task
322 that you want to be saved in the diary; when displaying, information like
323 the date (or date-related S-expressions) and UID can be left out.
324`ical-location' - the task's location, or geographical coordinates
325`ical-nonmarking' (a boolean) - if non-nil, the diary entry should be nonmarking
326`ical-organizer' - the task's organizer, formatted by
327 `diary-icalendar-attendee-format-function'
328`ical-percent-complete' (a number between 0 and 100) - the percentage of
329 the task which has already been completed
330`ical-priority' (a number) - the task's priority (1 = highest priority,
331 9 = lowest; 0 = undefined)
332`ical-rrule-sexp' - a string containing a diary S-expression for a
333 recurring task (only non-nil when importing). When this is non-nil,
334 you should normally use it instead of the start and end variables to
335 form the date of the entry.
336`ical-status' - overall status specified by the organizer (e.g. \"confirmed\")
337`ical-summary' - a summary of the task
338`ical-uid' - the unique identifier of the task
339`ical-url' - a URL for the task"
340 :version "31.1"
341 :type '(radio (function-item di:vjournal-skeleton)
342 (function :tag "Other function")))
343
344(defcustom di:import-predicate #'identity
345 "Predicate to filter iCalendar components before importing.
346
347This function must accept one argument, which will be an
348`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'
349component. It should return non-nil if this component should be
350formatted for import, or nil if it should be skipped.
351
352The default value will format all the events, todos, and journal entries
353in a given calendar."
354 :version "31.1"
355 :type '(radio (function-item identity)
356 (function :tag "Other predicate")))
357
358;;; Export customization
359(defgroup diary-icalendar-export nil
360 "iCalendar export from diary."
361 :version "31.1"
362 :group 'diary-icalendar
363 :prefix 'diary-icalendar)
364
365(defcustom di:address-regexp
366 (rx line-start
367 (one-or-more space)
368 (zero-or-one ;; property prefix, e.g. "Attendee:" or "Organizer:"
369 (seq (one-or-more word) ":"))
370 (group-n 2 (zero-or-more (not (any "<" "\n"))))
371 "<"
372 (group-n 1 (one-or-more (not (any "@" "\n")))
373 "@"
374 (one-or-more (not (any ">" "\n"))))
375 ">")
376 "Regular expression to match calendar user (email) addresses.
377
378The full address should match group 1; \"mailto:\" will be prepended to
379the full address during export, unless it or another URI scheme is
380present. If there is a match in group 2, it will be used as the
381common name associated with the address (see `icalendar-cnparam').
382
383The default value matches names and addresses on lines like:
384
385 Ms. Baz <baz@foo.com>
386
387as well as on lines like:
388
389 Property: Ms. Baz <baz@foo.com> other data...
390
391Any matching address within a diary entry will be exported as an
392iCalendar ATTENDEE property, unless the line on which it appears is also
393a match for `diary-icalendar-organizer-regexp', in which case it will be
394exported as the ORGANIZER property."
395 :version "31.1"
396 :type '(regexp))
397
398(defcustom di:description-regexp nil
399 "Regular expression to match description in an entry.
400
401If this is nil, the entire entry (after the date and time specification)
402is used as the description. Thus, it is only necessary to set this
403variable if you want to export diary entries where the text to be used
404as the description should not include the full entry body. In that case,
405the description should match group 1 of this regexp."
406 :version "31.1"
407 :type '(radio
408 (const :tag "Use full entry body" nil)
409 (regexp :tag "Regexp")))
410
411(defcustom di:organizer-regexp
412 (rx line-start
413 (one-or-more space)
414 "Organizer:")
415 "Regular expression to match line of an entry specifying the ORGANIZER.
416
417This regular expression need *not* match the name and address of the
418organizer (`diary-icalendar-address-regexp' is responsible for that).
419It only needs to match a line on which the organizer's address appears,
420to distinguish the organizer's address from other addresses."
421 :version "31.1"
422 :type '(regexp))
423
424(defcustom di:class-regexp
425 (rx line-start
426 (one-or-more space)
427 (or "Class:" ; for backward compatibility
428 "Access:")
429 (zero-or-more space)
430 (group-n 1 (or "public" "private" "confidential")))
431 "Regular expression to match access classification.
432
433The access classification value should be matched by group 1. The default
434regexp matches access classifications like:
435 Access: C
436or
437 Class: C
438where C can be any of:
439 public
440 private
441 confidential"
442 :version "31.1"
443 :type '(regexp))
444
445(defcustom di:location-regexp
446 (rx line-start
447 (one-or-more space)
448 "Location:"
449 (zero-or-more space)
450 (group-n 1 (one-or-more not-newline)))
451 "Regular expression to match location of an event.
452
453The location value should be matched by group 1. The default regexp
454matches lines like:
455
456 Location: Some place"
457 :version "31.1"
458 :type '(regexp))
459
460(defcustom di:status-regexp
461 (rx line-start
462 (one-or-more space)
463 "Status:"
464 (zero-or-more space)
465 (group-n 1 (or "tentative" "confirmed" "cancelled" "needs-action" "completed"
466 "in-process" "draft" "final")))
467 "Regular expression to match status of an event.
468
469The status value should be matched by group 1. The default regexp
470matches statuses on lines like:
471
472 Status: S
473
474where S can be any of:
475
476 tentative
477 confirmed
478 cancelled
479 needs-action
480 completed
481 in-process
482 draft
483 final"
484 :version "31.1"
485 :type '(regexp))
486
487(defcustom di:summary-regexp nil
488 "Regular expression to match summary in an entry.
489
490If this is nil, the first line of the entry (after the date and time
491specification) is used as the summary. Thus, it is only necessary to set
492this variable if you want to export diary entries where the text to be
493used as the summary does not appear on the first line of the entry. In
494that case, the summary should match group 1 of this regexp."
495 :version "31.1"
496 :type '(choice (const nil) regexp))
497
498(defcustom di:todo-regexp nil
499 "Regular expression that identifies an entry as a task (VTODO).
500
501If this is non-nil, any diary entry that matches this regexp will be
502exported as an iCalendar VTODO component (instead of VEVENT), with its
503due date equal to the entry date."
504 :version "31.1"
505 :type '(radio (const :tag "Do not export VTODO tasks" nil)
506 (regexp :tag "Regexp for tasks")))
507
508(defcustom di:uid-regexp
509 (rx line-start
510 (one-or-more space)
511 "UID:"
512 (zero-or-more space)
513 (group-n 1 (one-or-more not-newline)))
514 "Regular expression to match UID of an entry.
515
516The UID value should be matched by group 1. The default regexp matches
517UIDs on lines like:
518
519 UID: some-unique-identifier"
520 :version "31.1"
521 :type '(regexp))
522
523(defcustom di:url-regexp
524 (rx line-start
525 (one-or-more space)
526 "URL:"
527 (zero-or-more space)
528 (group-n 1 (eval 'ical:uri)))
529 "Regular expression to match URL of an entry.
530
531The full URL should be matched by group 1. The default regexp matches
532URLs on lines like:
533
534 URL: http://example.com/foo/bar"
535 :version "31.1"
536 :type '(regexp))
537
538(defcustom di:export-nonmarking-entries t
539 "Whether to export nonmarking diary entries.
540
541If this variable is nil, nonmarking diary entries (those prefixed with
542`diary-nonmarking-symbol') are never exported. If it is non-nil,
543nonmarking diary entries are exported; see also
544`diary-icalendar-export-nonmarking-as-vjournal' for more control over
545how they are exported."
546 :version "31.1"
547 :type '(choice (const :tag "Export nonmarking entries" t)
548 (const :tag "Do not export nonmarking entries" nil)))
549
550(defcustom di:export-nonmarking-as-vjournal nil
551 "Whether to export nonmarking diary entries as VJOURNAL components.
552
553If this variable is non-nil, nonmarking diary entries (those prefixed
554with `diary-nonmarking-symbol') will be exported as iCalendar VJOURNAL
555components, rather than VEVENT components. VJOURNAL components are
556intended to represent notes, documents, or other data associated with a
557date. External calendar applications may treat VJOURNAL components
558differently than VEVENTs, so consult your application's documentation
559before setting this variable to t.
560
561If this variable is nil, nonmarking entries will be exported as VEVENT
562components which do not take up busy time in the calendar (i.e., with
563the TRANSP property set to \"TRANSPARENT\"; see `icalendar-transp')."
564 :version "31.1"
565 :type '(choice (const :tag "Export nonmarking entries as VEVENT" nil)
566 (const :tag "Export nonmarking entries as VJOURNAL" t))
567 :link '(url-link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.3"))
568
569(defcustom di:export-alarms
570 nil
571 "Determine whether and how alarms are included in exported diary events.
572
573If this variable is nil, no alarms are created during export.
574If it is non-nil, it should be a list of lists like:
575
576\((TYPE LEAD-TIME [OPTIONS]) ...)
577
578In each inner list, the first element TYPE should be a symbol indicating
579an alarm type to generate: one of \\='audio, \\='display, or \\='email.
580The second element LEAD-TIME should be an integer specifying the amount
581of time before the event, in minutes, when the alarm should be
582triggered. For audio alarms, there are currently no other
583OPTIONS.
584
585For display and email alarms, the next OPTION is a format string for the
586displayed alarm, or the email subject line. In this string, \"%t\" will
587be replaced with LEAD-TIME and \"%s\" with the event's summary.
588
589If TYPE is \\='email, the next OPTION should be a list whose members
590specify the email addresses to which email alarms should be sent. These
591can either be email addresses (as strings), or the symbol
592\\='from-entry, meaning that these addresses should be taken from the
593exported diary entry (see `diary-icalendar-address-regexp')."
594 :version "31.1"
595 :type
596 '(choice (const :tag "Do not include alarms when exporting diary entries" nil)
597 (set :tag "Create alarms of these types"
598 (list :tag "Audio alarms"
599 (const :tag "Options" audio)
600 (integer :tag "Advance time (in minutes)"
601 :value 10)
602 ;; TODO: specify an audio file to attach?
603 ;; TODO: other options we could have here and below:
604 ;; - whether alarm is before event start or end
605 ;; - repetitions and delays between repetitions
606 )
607 (list :tag "Display alarms"
608 (const :tag "Options" display)
609 (integer :tag "Advance time (minutes)"
610 :value 10)
611 (string :tag "Display format"
612 :value "In %t minutes: %s")
613 ;; TODO: other options?
614 )
615 (list :tag "Email alarms"
616 (const :tag "Options" email)
617 (integer :tag "Advance time (minutes)"
618 :value 10)
619 ;; TODO: other options?
620 (string :tag "Subject line format"
621 :value "In %t minutes: %s")
622 (set
623 :tag "Attendees"
624 (const :tag "Parse addresses from entry"
625 from-entry)
626 (repeat :tag "Other addresses"
627 (string :tag "Email address")))))))
628
629(defcustom di:export-sexp-enumeration-days
630 14
631 "Number of days over which an S-expression diary entry is enumerated.
632
633Some S-expression entries cannot be translated to iCalendar format.
634They are therefore enumerated, i.e., explicitly evaluated for a
635certain number of days, and then exported. The enumeration starts
636on the current day and continues for the number of days given here.
637
638See `icalendar-export-sexp-enumerate-all' for a list of sexp
639entries which by default are NOT enumerated."
640 :version "31.1"
641 :type 'integer)
642
643(defcustom di:export-sexp-enumerate-all
644 nil
645 "Whether all S-expression diary entries are enumerated.
646
647If this variable is non-nil, all S-expression diary entries are
648enumerated for `diary-icalendar-export-sexp-enumeration-days' days
649instead of translating them into an iCalendar equivalent.
650This causes the following S-expression entries to be enumerated
651instead of translated to a recurrence rule:
652 `diary-anniversary'
653 `diary-block'
654 `diary-cyclic'
655 `diary-date'
656 `diary-float'
657 `diary-remind'
658 `diary-rrule'
659 `diary-time-block'
660All other S-expression entries are enumerated in any case."
661 :version "31.1"
662 :type '(choice (const :tag "Export without enumeration when possible" nil)
663 (const :tag "Always enumerate S-expression entries" t)))
664
665(defcustom di:recurring-start-year
666 (1- (decoded-time-year (decode-time)))
667 "Start year for recurring events.
668
669Set this to a year just before the start of your personal calendar.
670It is needed when exporting certain diary S-expressions to iCalendar
671recurring events, and because some calendar browsers only propagate
672recurring events for several years beyond the start time."
673 :version "31.1"
674 :type 'integer)
675
676(defcustom di:time-zone-export-strategy
677 'local
678 "Strategy to use for exporting clock times in diary files.
679
680The symbol `local' (the default) means to assume that times are in the
681time zone determined by `calendar-current-time-zone'. The time zone
682information returned by that function will be exported as an iCalendar
683VTIMEZONE component, and clock times in the diary file will be exported
684with a reference to that time zone definition.
685
686On some systems, `calendar-current-time-zone' cannot determine time zone
687information for the local time zone. In that case, you can set this
688variable to a list in the format returned by that function:
689
690 (UTC-DIFF DST-OFFSET STD-ZONE DST-ZONE
691 DST-STARTS DST-ENDS DST-STARTS-TIME DST-ENDS-TIME)
692
693This list describes the time zone you would like to use for export. See
694the docstring of `calendar-current-time-zone' for details. Times in the
695diary file will be exported like with `local' for this time zone.
696
697The other possible values for this variable avoid the need to include
698any time zone information in the exported iCalendar data:
699
700The symbol `to-utc' means to re-encode all exported times to UTC
701time. In this case, export will assume that times are in Emacs local
702time, and rely on `encode-time' and `decode-time' to convert them to UTC
703times.
704
705The symbol `floating' means to export clock times without any time
706zone identifier, which the iCalendar standard (RFC5545) calls
707\"floating\" times. RFC5545 specifies that floating times should be
708interpreted as local to whichever time zone the recipient of the
709iCalendar data is currently in (which might be different from your local
710time zone). You should only use this if that behavior makes sense for
711the events you are exporting."
712 :version "31.1"
713 :type
714 '(radio (const :tag "Use TZ from `calendar-current-time-zone'" local)
715 (const :tag "Convert local times to UTC" to-utc)
716 (const :tag "Use floating times" floating)
717 (sexp :tag "User-provided TZ information"
718 :match icr:-tz-info-sexp-p
719 :type-error
720 "See `calendar-current-time-zone' for format"))
721 :link '(url-link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5"))
722
723(defcustom di:export-linewise
724 nil
725 "Export entries with multiple lines to distinct events.
726
727If this is non-nil, each line of a diary entry will be exported as a
728separate iCalendar event.
729
730If you write your diary entries in a one-entry-per-day style, with
731multiple events or appointments per day, you can use this variable to
732export these individual events to iCalendar format. For example, an
733entry like:
734
7352025-05-03
736 9AM Lab meeting
737 Günter to present on new assay
738 Start experiment A
739 12:30-1:30PM Lunch with Phil
740 16:00 Experiment A finishes; move to freezer
741
742will be exported as four events, each on 2025-05-03 but with different
743start times (except for the second event, \"Start experiment A\", which
744has no start time). An event line can be continued onto subsequent lines
745via additional indentation, as in the first event in this entry.
746
747If this variable is non-nil, each distinct event must begin on a
748continuation line of the entry (below the date); any text on the same
749line as the date is ignored. A time specification can only appear at
750the beginning of each continuation line of the entry, immediately after
751the leading whitespace.
752
753If this variable is nil, each entry will be exported as exactly one
754event, and only a time specification immediately following the date will
755determine the start and end times for that event. Thus, in the example
756above, the exported event would have a start date but no start time or
757end time. The times in the entry would be preserved as text in the
758event description."
759 :version "31.1"
760 :type '(radio (const :tag "Do not export linewise" nil)
761 (const :tag "Export linewise" t)))
762
763(defcustom di:other-properties-parser nil
764 "Function to parse additional iCalendar properties from diary entries.
765
766If you like to keep your diary entries in a particular format, you can
767set this to a function which parses that format to iCalendar properties
768during iCalendar export, so that other calendar applications can use
769them.
770
771The parsing function will be called with the current restriction set to
772the boundaries of a diary entry. If `diary-icalendar-export-linewise'
773is non-nil, the restriction will correspond to a single event in a
774multi-line diary entry.
775
776The function should accept two arguments, TYPE and PROPERTIES. TYPE is
777the iCalendar type symbol (one of \\='icalendar-vevent,
778\\='icalendar-vjournal, or \\='icalendar-vtodo) for the component being
779generated for the entry. PROPERTIES is the list of property nodes that
780`diary-icalendar-parse-entry' has already parsed from the entry and will
781be included in the exported component.
782
783The function should return a list of iCalendar property nodes, which
784(in addition to PROPERTIES) will be incorporated into the
785`icalendar-vevent', `icalendar-vjournal', or `icalendar-vtodo' component
786node created from the current entry. See the docstrings of those
787symbols for more information on the properties they can contain, and the
788`icalendar-make-property' macro for a simple way to create property
789nodes from values parsed from the entry."
790 :version "31.1"
791 :type '(radio (const :tag "Do not parse additional properties" nil)
792 (function :tag "Parsing function")))
793
794
795;; Utilities for display and import
796
797;;; Error handling
798(define-error 'ical:diary-import-error "Unable to import iCalendar data"
799 'ical:error)
800
801(cl-defun di:signal-import-error (msg &key (diary-buffer (current-buffer))
802 (position (point))
803 line
804 (severity 2))
805 (let ((err-data
806 (list :message msg
807 :buffer diary-buffer
808 :position position
809 :line line
810 :severity severity)))
811 (signal 'ical:diary-import-error err-data)))
812
813;;; Backward compatibility with icalendar.el
814
815;; icalendar.el provided the following customization variables:
816;; `icalendar-import-format'
817;; `icalendar-import-format-class'
818;; `icalendar-import-format-description'
819;; `icalendar-import-format-location'
820;; `icalendar-import-format-organizer'
821;; `icalendar-import-format-summary'
822;; `icalendar-import-format-status'
823;; `icalendar-import-format-url'
824;; `icalendar-import-format-uid'
825;; These were all format strings: `icalendar-import-format' was the
826;; top-level format string, which would potentially incorporate the
827;; formatted output from the others. This approach to customization
828;; isn't very flexible, though, and doing it right requires a
829;; separate defcustom variable for each iCalendar property. (The above
830;; list is not nearly exhaustive.) I have abandoned this approach in
831;; what follows in favor of skeleton.el templates, but the following two
832;; functions provide backward compatibility for anyone who had
833;; customized the values of the above variables:
834(defun di:-use-legacy-vars-p ()
835 "Return non-nil if user has set `icalendar-import-format*' variables.
836If any of these variables have non-default values, they will be used by
837`diary-icalendar-import-format-entry' to import events. This function
838is for backward compatibility; please do not rely on it in new code."
839 (declare (obsolete nil "31.1"))
840 (with-suppressed-warnings
841 ((obsolete ical:import-format
842 ical:import-format-class
843 ical:import-format-description
844 ical:import-format-location
845 ical:import-format-organizer
846 ical:import-format-summary
847 ical:import-format-status
848 ical:import-format-url
849 ical:import-format-uid))
850 (or
851 (and (boundp 'ical:import-format)
852 (not (equal ical:import-format
853 (custom--standard-value 'ical:import-format))))
854 (and (boundp 'ical:import-format-class)
855 (not (equal ical:import-format-class
856 (custom--standard-value 'ical:import-format-class))))
857 (and (boundp 'ical:import-format-description)
858 (not (equal ical:import-format-description
859 (custom--standard-value
860 'ical:import-format-description))))
861 (and (boundp 'ical:import-format-location)
862 (not (equal ical:import-format-location
863 (custom--standard-value 'ical:import-format-location))))
864 (and (boundp 'ical:import-format-organizer)
865 (not (equal ical:import-format-organizer
866 (custom--standard-value 'ical:import-format-organizer))))
867 (and (boundp 'ical:import-format-summary)
868 (not (equal ical:import-format-summary
869 (custom--standard-value 'ical:import-format-summary))))
870 (and (boundp 'ical:import-format-status)
871 (not (equal ical:import-format-status
872 (custom--standard-value 'ical:import-format-status))))
873 (and (boundp 'ical:import-format-url)
874 (not (equal ical:import-format-url
875 (custom--standard-value 'ical:import-format-url))))
876 (and (boundp 'ical:import-format-uid)
877 (not (equal ical:import-format-uid
878 (custom--standard-value 'ical:import-format-uid)))))))
879
880(defun di:-format-vevent-legacy (date class desc location organizer
881 summary status url uid)
882 "Format an entry on DATE using the values of obsolete import variables.
883This function is for backward compatibility; please do not rely on it in
884new code."
885 (declare (obsolete nil "31.1"))
886 (with-suppressed-warnings
887 ((obsolete ical:import-format
888 ical:import-format-class
889 ical:import-format-description
890 ical:import-format-location
891 ical:import-format-organizer
892 ical:import-format-summary
893 ical:import-format-status
894 ical:import-format-url
895 ical:import-format-uid))
896
897 (insert ical:import-format)
898 (replace-regexp-in-region "%c"
899 (format ical:import-format-class class)
900 (point-min) (point-max))
901 (replace-regexp-in-region "%d"
902 (format ical:import-format-description desc)
903 (point-min) (point-max))
904 (replace-regexp-in-region "%l"
905 (format ical:import-format-location location)
906 (point-min) (point-max))
907 (replace-regexp-in-region "%o"
908 (format ical:import-format-organizer organizer)
909 (point-min) (point-max))
910 (replace-regexp-in-region "%s"
911 (format ical:import-format-summary summary)
912 (point-min) (point-max))
913 (replace-regexp-in-region "%t"
914 (format ical:import-format-status status)
915 (point-min) (point-max))
916 (replace-regexp-in-region "%u"
917 (format ical:import-format-url url)
918 (point-min) (point-max))
919 (replace-regexp-in-region "%U"
920 (format ical:import-format-uid uid)
921 (point-min) (point-max))
922 (goto-char (point-min))
923 (insert date " ")))
924
925(defun di:-vevent-to-legacy-alist (vevent)
926 "Convert an `icalendar-vevent' to an alist of the kind used by icalendar.el.
927This function is for backward compatibility; please do not rely on it in
928new code."
929 (declare (obsolete nil "31.1"))
930 ;; function values of `icalendar-import-format' expect a list like:
931 ;; ((VEVENT nil
932 ;; ((PROP1 params val)
933 ;; (PROP2 params val)
934 ;; ...)))
935 (let ((vevent-children (ical:ast-node-children vevent))
936 children)
937 (dolist (p vevent-children)
938 (let* ((type (ical:ast-node-type p))
939 (list-sep (get type 'ical:list-sep))
940 (name (intern (car (rassq type ical:property-types))))
941 ;; icalendar.el did not interpret values when parsing, so we
942 ;; convert back to string representation:
943 (value (ical:ast-node-value p))
944 (value-str
945 (or (ical:ast-node-meta-get :original-value p)
946 (if list-sep
947 (string-join (mapcar #'ical:default-value-printer value)
948 list-sep)
949 (ical:default-value-printer value))))
950 params)
951 (when (ical:ast-node-children p)
952 (dolist (param (ical:ast-node-children p))
953 (let* ((par-str (ical:print-param-node param))
954 (split (string-split par-str "[;=]"))
955 (parname (intern (nth 1 split)))
956 (parval (nth 2 split)))
957 (push `(,parname nil ,parval) params)))
958 (setq params (nreverse params)))
959 (push `(,name ,params ,value-str) children)))
960 (setq children (nreverse children))
961 ;; Return the legacy alist:
962 `((VEVENT nil ,children))))
963
964;;; Other utilities
965
966(defconst di:entry-regexp
967 (rx line-start
968 (group-n 1 ; first line of entry
969 (or (group-n 2 (regexp diary-nonmarking-symbol))
970 (not (any "\t\n #")))
971 (one-or-more not-newline))
972 (group-n 3 ; continuation lines of entry
973 (zero-or-more "\n" (any space) (zero-or-more not-newline))))
974 "Regular expression to match a full diary entry.
975
976Group 1 matches the first line of the entry. Group 2 contains
977`diary-nonmarking-symbol', if it was present at the start of the first
978line. Group 3 contains any continuation lines of the entry.")
979
980;; TODO: move to diary-lib.el?
981(defun di:entry-bounds ()
982 "Return markers (START END) bounding the diary entry around point.
983If point is not in an entry, return nil."
984 (save-excursion
985 (let* ((pt (point))
986 (bound (point-min))
987 (start (make-marker))
988 (end (make-marker)))
989 (when (re-search-backward "^[[:space:]]*$" nil t)
990 (setq bound (match-end 0)))
991 (goto-char pt)
992 (cond ((looking-at di:entry-regexp)
993 (set-marker start (match-beginning 0))
994 (set-marker end (match-end 0)))
995 ((re-search-backward di:entry-regexp bound t)
996 (set-marker start (match-beginning 0))
997 ;; match again forward, to ensure we get the full entry;
998 ;; see `re-search-backward':
999 (goto-char start)
1000 (when (looking-at di:entry-regexp)
1001 (set-marker end (match-end 0))))
1002 (t nil))
1003 (when (and (marker-position start) (marker-position end))
1004 (list start end)))))
1005
1006(defun di:find-entry-with-uid (uid &optional diary-filename)
1007 "Search DIARY-FILENAME (default: `diary-file') for an entry containing UID.
1008
1009The UID must occur on a line matching `diary-icalendar-uid-regexp'. If
1010such an entry exists, return markers (START END) bounding it.
1011Otherwise, return nil."
1012 (let* ((diary-file (or diary-filename diary-file))
1013 (diary-buffer (or (find-buffer-visiting diary-file)
1014 (find-file-noselect diary-file))))
1015 (with-current-buffer diary-buffer
1016 (save-excursion
1017 (save-restriction
1018 (widen)
1019 (goto-char (point-min))
1020 (catch 'found
1021 (while (re-search-forward di:uid-regexp nil t)
1022 (when (equal uid (match-string 1))
1023 (throw 'found (di:entry-bounds))))
1024 ;; continue search in included files:
1025 ;; TODO: is this a good idea?
1026 ;; (goto-char (point-min))
1027 ;; (while (re-search-forward
1028 ;; (rx line-start (regexp diary-include-string)
1029 ;; ?\" (group-n 1 (one-or-more (not ?\")) ?\"))
1030 ;; nil t)
1031 ;; (let ((entry (di:find-entry-with-uid uid (match-string 1))))
1032 ;; (when entry
1033 ;; (throw 'found entry))))
1034 ;; nothing to return:
1035 nil))))))
1036
1037(defun di:y-or-n-or-edit-p (prompt)
1038 "Like `y-or-n-p', but with the option to enter a recursive edit.
1039Adds a message to current binding of `help-form' explaining how."
1040 (let* ((allow-edits-map
1041 (let ((map (make-sparse-keymap)))
1042 (define-key map [remap edit]
1043 (lambda ()
1044 (interactive)
1045 (save-excursion
1046 (save-window-excursion
1047 (recursive-edit)))))
1048 map))
1049 (y-or-n-p-map (make-composed-keymap allow-edits-map
1050 y-or-n-p-map))
1051 (help-form
1052 (concat (when (stringp help-form) (concat help-form "\n\n"))
1053 ;; FIXME: should use substitute-command-keys here, but
1054 ;; for some reason, even with \<y-or-n-p-map>, it
1055 ;; doesn't find the C-r and C-M-c bindings and only
1056 ;; suggests M-x ...
1057 "Type C-r to enter recursive edit before answering "
1058 "(C-M-c to exit).")))
1059 (save-excursion
1060 (save-restriction
1061 (y-or-n-p prompt)))))
1062
1063;;; Skeletons
1064;;
1065;; We use skeleton.el's templating facilities to make formatting of
1066;; different iCalendar elements in the diary simple and easy to
1067;; customize. There are default skeletons for each major type of
1068;; iCalendar component (`di:vevent-skeleton', `di:vtodo-skeleton',
1069;; `di:vjournal-skeleton'), and a corresponding defcustom pointing to
1070;; each of these skeletons (`di:vevent-format-function', etc.).
1071;; `di:format-entry' calls these skeletons, or user-provided functions,
1072;; to format individual components as diary entries. Since properties
1073;; representing people (`icalendar-attendee', `icalendar-organizer') are
1074;; important and relatively complex, another skeleton
1075;; (`di:attendee-skeleton') takes care of formatting these for the
1076;; top-level component skeletons.
1077(defun di:attendee-skeleton (attendee)
1078 "Default skeleton to format an `icalendar-attendee' for the diary.
1079
1080Includes any data from the attendee's `icalendar-cnparam' and
1081`icalendar-partstatparam', and does not insert any data if its
1082`icalendar-cutypeparam' is non-nil and anything other than
1083\"INDIVIDUAL\" or \"GROUP\".
1084
1085The result looks like:
1086 <foo@example.com>
1087or
1088 Baz Foo <foo@example.com>
1089or
1090 Baz Foo <foo@example.com> (declined)"
1091 (ignore attendee) ; we only need the `attendee-' vars below
1092 (with-suppressed-warnings ((free-vars attendee-cutype))
1093 ;; skip non-human "attendees":
1094 (when (or (not attendee-cutype)
1095 (equal attendee-cutype "INDIVIDUAL")
1096 (equal attendee-cutype "GROUP"))
1097 (skeleton-insert
1098 '(nil
1099 attendee-cn
1100 (format " <%s>" attendee-address)
1101 (when attendee-partstat
1102 (format " (%s)" (downcase attendee-partstat))))))))
1103
1104(defun di:format-attendee (attendee)
1105 "Format ATTENDEE for the diary.
1106
1107ATTENDEE should be an `icalendar-attendee' or `icalendar-organizer'
1108property node. Returns a string representing an entry for the attendee,
1109formatted by `diary-icalendar-attendee-format-function', unless the
1110attendee's address matches the regexp in
1111`diary-icalendar-skip-addresses-regexp'; in that case, nil is returned."
1112 (ical:with-property attendee
1113 ((ical:cutypeparam :value cutype)
1114 (ical:cnparam :value cn)
1115 (ical:roleparam :value role)
1116 (ical:partstatparam :value partstat)
1117 (ical:rsvpparam :value rsvp))
1118 (unless (and di:skip-addresses-regexp
1119 (string-match-p di:skip-addresses-regexp value))
1120 (dlet ((attendee-address (ical:strip-mailto value))
1121 (attendee-cn (when cn (string-trim cn)))
1122 (attendee-cutype cutype)
1123 (attende-role role)
1124 (attendee-partstat partstat)
1125 (attendee-rsvp rsvp))
1126 (with-temp-buffer
1127 (funcall di:attendee-format-function attendee)
1128 (buffer-string))))))
1129
1130(defun di:vevent-skeleton (vevent)
1131 "Default skeleton to format an `icalendar-vevent' for the diary."
1132 (ignore vevent) ; we only need the dynamic `ical-*' variables here
1133 (skeleton-insert
1134 '(nil
1135 (when (or ical-nonmarking (equal ical-transparency "TRANSPARENT"))
1136 diary-nonmarking-symbol)
1137 (or ical-rrule-sexp ical-start-to-end ical-start) & " "
1138 ical-summary "\n"
1139 @ ; start of body (for indentation)
1140 (when ical-location "Location: ") ical-location
1141 & "\n" (when ical-url "URL: ") & ical-url
1142 & "\n" (when ical-status "Status: ") & ical-status
1143 & "\n" (when ical-organizer "Organizer: ") & ical-organizer
1144 & "\n" (di:format-list ical-attendees "Attendee")
1145 & "\n" (di:format-list ical-categories "Category" "Categories")
1146 & "\n" (di:format-list ical-comments "Comment")
1147 & "\n" (di:format-list ical-contacts "Contact")
1148 & "\n" (di:format-list ical-attachments "Attachment")
1149 & "\n" (when (and ical-importing ical-access) "Access: ") & ical-access
1150 & "\n" (when (and ical-importing ical-uid) "UID: ") & ical-uid
1151 & "\n" (when ical-description "Description: ") & ical-description
1152 & "\n"
1153 @ ; end of body
1154 (let* ((end (pop skeleton-positions))
1155 (start (pop skeleton-positions)))
1156 ;; TODO: should diary define a customizable indentation level?
1157 ;; For now, we use 1 because that's what icalendar.el chose
1158 (indent-code-rigidly start end 1)
1159 nil) ; Don't insert return value
1160 (when ical-importing "\n"))))
1161
1162(defun di:vjournal-skeleton (vjournal)
1163 "Default skeleton to format an `icalendar-vjournal' for the diary."
1164 (ignore vjournal) ; we only need the dynamic `ical-*' variables here
1165 (skeleton-insert
1166 '(nil
1167 (when (or ical-nonmarking di:import-vjournal-as-nonmarking)
1168 diary-nonmarking-symbol)
1169 (or ical-rrule-sexp ical-start) & " "
1170 ical-summary "\n"
1171 @ ; start of body (for indentation)
1172 & "\n" (when ical-url "URL: ") & ical-url
1173 & "\n" (when ical-status "Status: ") & ical-status
1174 & "\n" (when ical-organizer "Organizer: ") & ical-organizer
1175 & "\n" (di:format-list ical-attendees "Attendee")
1176 & "\n" (di:format-list ical-categories "Category" "Categories")
1177 & "\n" (di:format-list ical-comments "Comment")
1178 & "\n" (di:format-list ical-contacts "Contact")
1179 & "\n" (di:format-list ical-attachments "Attachment")
1180 & "\n" (when (and ical-importing ical-access) "Access: ") & ical-access
1181 & "\n" (when (and ical-importing ical-uid) "UID: ") & ical-uid
1182 ;; In a vjournal, multiple `icalendar-description's are allowed:
1183 & "\n" (di:format-list ical-descriptions "Description")
1184 & "\n"
1185 @ ; end of body
1186 (let* ((end (pop skeleton-positions))
1187 (start (pop skeleton-positions)))
1188 (indent-code-rigidly start end 1)
1189 nil) ; Don't insert return value
1190 (when ical-importing "\n"))))
1191
1192(defun di:vtodo-skeleton (vtodo)
1193 "Default skeleton to format an `icalendar-vtodo' for the diary."
1194 (ignore vtodo) ; we only need the dynamic `ical-*' variables here
1195 (skeleton-insert
1196 '(nil
1197 (when ical-nonmarking diary-nonmarking-symbol)
1198 (or ical-rrule-sexp ical-due) & " "
1199 (when ical-due "Due: ") summary
1200 (when start (concat " (Start: " ical-start ")"))
1201 "\n"
1202 @ ; start of body (for indentation)
1203 & "\n" (when ical-url "URL: ") & ical-url
1204 & "\n" (when ical-status "Status: ") & ical-status
1205 & "\n" (when ical-completed "Completed: ") & ical-completed
1206 & "\n" (when ical-percent-complete
1207 (format "Progress: %d%%" ical-percent-complete))
1208 & "\n" (when ical-organizer "Organizer: ") & ical-organizer
1209 & "\n" (di:format-list ical-attendees "Attendee")
1210 & "\n" (di:format-list ical-categories "Category" "Categories")
1211 & "\n" (di:format-list ical-comments "Comment")
1212 & "\n" (di:format-list ical-contacts "Contact")
1213 & "\n" (di:format-list ical-attachments "Attachment")
1214 & "\n" (when (and ical-importing ical-access) "Access: ") & ical-access
1215 & "\n" (when (and ical-importing ical-uid) "UID: ") & ical-uid
1216 & "\n" (when ical-description "Description: ") & ical-description
1217 & "\n"
1218 @ ; end of body
1219 (let* ((end (pop skeleton-positions))
1220 (start (pop skeleton-positions)))
1221 (indent-code-rigidly start end 1)
1222 nil) ; Don't insert return value
1223 (when ical-importing "\n"))))
1224
1225;;; Further utilities for formatting/importing special kinds of values:
1226(defun di:format-geo-coordinates (geo)
1227 "Format an `icalendar-geo-coordinates' value as degrees N/S and E/W."
1228 (format "%.6f°%s %.6f°%s" ; RFC5545 says we may truncate after 6 decimal places
1229 (abs (car geo)) (if (< 0 (car geo)) "N" "S")
1230 (abs (cdr geo)) (if (< 0 (cdr geo)) "E" "W")))
1231
1232(defun di:save-binary-attachment (base64-data dir &optional mimetype)
1233 "Decode and save BASE64-DATA to a new file in DIR.
1234
1235The file will be named based on a unique prefix of BASE64-DATA with an
1236extension based on MIMETYPE. It will be saved in a subdirectory named
1237DIR of `diary-icalendar-attachment-directory', which will be created if
1238necessary. Returns the (non-directory part of) the saved filename."
1239 (require 'mailcap) ; for `mailcap-mime-type-to-extension'
1240 ;; Create the subdirectory for the attachment if necessary:
1241 (unless (and (directory-name-p di:attachment-directory)
1242 (file-writable-p di:attachment-directory))
1243 (di:signal-import-error
1244 (format "Cannot write to directory: %s" di:attachment-directory)))
1245 (make-directory (file-name-concat di:attachment-directory dir) t)
1246 ;; Create a unique filename for the attachment. Unfortunately RFC5545
1247 ;; has no mechanism for suggesting a filename, so we just use a unique
1248 ;; prefix of BASE64-DATA, or a random number as a fallback.
1249 (let* ((nchars 4)
1250 (max-chars (length base64-data))
1251 (prefix (substring base64-data 0 nchars))
1252 (extn (when mimetype
1253 (concat "." (symbol-name
1254 (mailcap-mime-type-to-extension mimetype)))))
1255 (path (file-name-concat di:attachment-directory dir
1256 (concat prefix extn))))
1257 (while (file-exists-p path)
1258 (cl-incf nchars)
1259 (setq prefix (if (< nchars max-chars)
1260 (substring base64-data 0 nchars)
1261 (number-to-string (random max-chars))))
1262 (setq path (file-name-concat di:attachment-directory dir
1263 (concat prefix extn))))
1264 ;; Save the file and return its name:
1265 (let ((data (base64-decode-string base64-data))
1266 (coding-system-for-write 'no-conversion))
1267 (write-region data nil path)
1268 (file-name-nondirectory path))))
1269
1270(defun di:save-attachments-from (attachment-nodes uid)
1271 "Save attachments in ATTACHMENT-NODES and return a list of attachments.
1272
1273If these nodes contain binary data, rather than an URL, save the data to
1274a file in `diary-icalendar-attachment-directory' (unless this variable
1275is nil). UID should be the universal ID of the component containing
1276ATTACHMENT-NODES; the attachments will be saved in a subdirectory of the
1277same name. The returned list is a list of strings, which are either
1278URLs or filenames."
1279 (let (entry-attachments)
1280 (dolist (node attachment-nodes)
1281 (ical:with-property node
1282 ((ical:fmttypeparam :value fmttype))
1283 (when (and (eq 'ical:binary value-type)
1284 di:attachment-directory)
1285 (let ((filename (di:save-binary-attachment value uid fmttype)))
1286 (push filename entry-attachments)))
1287 (when (eq 'ical:url value-type)
1288 (push value entry-attachments))))
1289 ;; Return the list of filenames and URLs:
1290 entry-attachments))
1291
1292(defun di:format-list (values &optional title plural-form sep indent)
1293 "Smartly format VALUES for the diary.
1294
1295VALUES should be a list of strings. nil elements will be ignored, and an
1296empty list will return nil.
1297
1298TITLE is a string to add to the beginning of the list; a colon will be
1299appended. PLURAL-FORM is the plural of TITLE, to be used when VALUES
1300contains more than one element (default: TITLE+\"s\").
1301
1302The strings in VALUES are first joined with SEP (default: \", \"), with
1303\"TITLE: \" prepended. If the result is longer than the current value of
1304`fill-column', the values are instead formatted one per line, with the
1305title on its own line at the beginning, and the whole list indented
1306relative to the title by INDENT spaces (default: 2). Thus, in the first
1307case, the result looks like:
1308 TITLE(s): VAL1, VAL2, ...
1309and in the second:
1310 TITLE(s):
1311 VAL1
1312 VAL2
1313 ..."
1314 (when (cdr values)
1315 (setq title (when title (or plural-form (concat title "s")))))
1316 (unless indent
1317 (setq indent 2))
1318 ;; Remove nil values and extra whitespace:
1319 (setq values (mapcar #'string-trim (delq nil values)))
1320 (when values
1321 (let ((line (concat
1322 (when title (concat title ": "))
1323 (string-join values (or sep ", ")))))
1324 (if (< (length line) fill-column)
1325 line
1326 ;; Otherwise, one value per line:
1327 (with-temp-buffer
1328 (insert (string-join values "\n"))
1329 (indent-code-rigidly (point-min) (point-max) indent)
1330 (goto-char (point-min))
1331 (when title
1332 (insert title ":\n"))
1333 (buffer-string))))))
1334
1335(defun di:format-time (dt &optional tzname)
1336 "Format the `icalendar-date-time' DT for the diary.
1337The time is formatted according to `diary-icalendar-time-format', which see.
1338TZNAME, if specified, should be a string naming the time zone observance
1339in which DT occurs."
1340 ;; Diary does not support seconds, so silently truncate:
1341 (let ((time (format-time-string di:time-format (encode-time dt))))
1342 (if tzname
1343 (concat time " " tzname)
1344 time)))
1345
1346(defun di:format-time-as-local (dt &optional original-tzname)
1347 "Format the time in `icalendar-date-time' DT for the diary.
1348
1349DT is translated to the system local time zone if necessary, and the
1350original time specification is preserved in parentheses if it was given
1351in a different zone. ORIGINAL-TZNAME, if specified, should be a string
1352naming the time zone observance in which DT was originally encoded in
1353the iCalendar data."
1354 (cl-typecase dt
1355 (ical:date "")
1356 (ical:date-time
1357 (let* ((ts (encode-time dt))
1358 (original-offset (decoded-time-zone dt))
1359 (local-tz (current-time-zone ts))
1360 (local-offset (car local-tz))
1361 (local-dt (decode-time ts local-tz))
1362 (local-str (di:format-time local-dt)))
1363 (if (and original-tzname original-offset
1364 (not (= original-offset local-offset)))
1365 (format "%s (%s)" local-str (di:format-time dt original-tzname))
1366 local-str)))))
1367
1368(defun di:format-date (dt)
1369 "Format the `icalendar-date' or `icalendar-date-time' DT for the diary.
1370If DT is a date-time, only the date part is considered. The date is
1371formatted with `calendar-date-string' according to the pattern in
1372`diary-date-insertion-form'."
1373 (dlet ((calendar-date-display-form diary-date-insertion-form))
1374 (cl-typecase dt
1375 (ical:date (calendar-date-string dt t t))
1376 (ical:date-time (calendar-date-string (ical:date-time-to-date dt) t t)))))
1377
1378(defun di:format-date/time-as-local (dt &optional original-tzname)
1379 "Format the `icalendar-date' or `icalendar-date-time' DT for the diary.
1380
1381If DT is a plain date, only the date will be formatted. If DT is a
1382date-time, both the date and the time will formatted, after translating
1383DT into a date and time into the system local time.
1384
1385If specified, ORIGINAL-TZNAME should be a string naming the time zone
1386observance in which DT was originally encoded in the iCalendar data. In
1387this case, the original clock time in DT will also be added in
1388parentheses, with date if necessary. For example:
1389 2025/05/01 09:00 (08:00 GMT)
1390or
1391 2025/05/01 18:00 (2025/05/02 08:00 JST)"
1392 (let ((local-dt (ical:date/time-to-local dt)))
1393 (cl-typecase local-dt
1394 (ical:date (di:format-date local-dt))
1395 (ical:date-time
1396 (let ((date (di:format-date local-dt))
1397 (time (di:format-time local-dt))
1398 (orig-date (di:format-date dt))
1399 (orig-time (di:format-time dt original-tzname)))
1400 (if original-tzname
1401 (format "%s %s (%s)" date time
1402 (if (equal date orig-date)
1403 orig-time
1404 (format "%s %s" orig-date orig-time)))
1405 (format "%s %s" date time)))))))
1406
1407(defun di:format-time-range (start end &optional omit-start-date)
1408 "Format a time range for the diary.
1409
1410START and END should be `icalendar-date-time' values where the date part
1411is the same. (If they are not on the same date, nil is returned; use
1412`diary-icalendar-format-time-block-sexp' to make a diary S-exp for this
1413range instead.)
1414
1415The date is only formatted once, and the time is formatted as a range, like:
1416 STARTDATE STARTTIME-ENDTIME
1417If OMIT-START-DATE is non-nil, STARTDATE will be omitted."
1418 (when (equal (ical:date/time-to-date start) (ical:date/time-to-date end))
1419 (format "%s%s-%s"
1420 (if omit-start-date ""
1421 (concat (di:format-date start) " "))
1422 (di:format-time-as-local start)
1423 (di:format-time-as-local end))))
1424
1425(defun di:format-block-sexp (start end)
1426 "Format a `diary-block' diary S-expression between START and END.
1427
1428START and END may be `icalendar-date' or `icalendar-date-time'
1429values. If they are date-times, only the date parts will be considered.
1430Returns a string like \"%%(diary-block ...)\" with the arguments properly
1431ordered for the current value of `calendar-date-style'."
1432 (unless (cl-typep start 'ical:date)
1433 (setq start (ical:date-time-to-date start)))
1434 (unless (cl-typep end 'ical:date)
1435 (setq end (ical:date-time-to-date end)))
1436 (concat
1437 diary-sexp-entry-symbol
1438 (apply #'format "(diary-block %d %d %d %d %d %d)"
1439 (cl-case calendar-date-style
1440 ;; M/D/Y
1441 (american (list (calendar-extract-month start)
1442 (calendar-extract-day start)
1443 (calendar-extract-year start)
1444 (calendar-extract-month end)
1445 (calendar-extract-day end)
1446 (calendar-extract-year end)))
1447 ;; D/M/Y
1448 (european (list (calendar-extract-day start)
1449 (calendar-extract-month start)
1450 (calendar-extract-year start)
1451 (calendar-extract-day end)
1452 (calendar-extract-month end)
1453 (calendar-extract-year end)))
1454 ;; Y/M/D
1455 (iso (list (calendar-extract-year start)
1456 (calendar-extract-month start)
1457 (calendar-extract-day start)
1458 (calendar-extract-year end)
1459 (calendar-extract-month end)
1460 (calendar-extract-day end)))))))
1461
1462(defun di:format-time-block-sexp (start end)
1463 "Format a `diary-time-block' diary S-expression for times between START and END."
1464 (concat
1465 diary-sexp-entry-symbol
1466 (format "(diary-time-block :start '%s :end '%s)" start end)))
1467
1468(defun di:format-rrule-sexp (component)
1469 "Format the recurrence rule data in COMPONENT as a diary S-expression.
1470
1471The returned string looks like \"%%(diary-rrule ...)\", and contains the
1472necessary data from COMPONENT for the calendar to compute recurrences of
1473the event."
1474 (ical:with-component component
1475 ((ical:dtstart :value dtstart)
1476 (ical:dtend :value dtend)
1477 (ical:duration :value duration)
1478 (ical:rrule :value rrule)
1479 (ical:rdate :all rdate-nodes)
1480 (ical:exdate :all exdate-nodes))
1481 (unless (or rrule rdate-nodes)
1482 (di:signal-import-error "No recurrence data in component"))
1483 (let ((exdates
1484 (mapcar #'ical:ast-node-value
1485 (apply #'append
1486 (mapcar #'ical:ast-node-value exdate-nodes))))
1487 (rdates
1488 (mapcar #'ical:ast-node-value
1489 (apply #'append
1490 (mapcar #'ical:ast-node-value rdate-nodes))))
1491 ;; N.B. we intentionally *don't* add any clock times to the
1492 ;; imported diary entry, since they could conflict with the
1493 ;; times generated by the recurrence rule, e.g. if the rule is
1494 ;; an 'HOURLY rule. Instead we always specify the end time
1495 ;; (if any) via a duration, and take care of displaying the
1496 ;; correct clocks times after computing recurrences during
1497 ;; diary display (see `diary-rrule').
1498 (dur-value (cond (duration duration)
1499 (dtend (unless (equal dtstart dtend)
1500 (ical:duration-between dtstart dtend)))
1501 (t nil)))
1502 (arg-plist nil))
1503
1504 (when exdates
1505 (setq arg-plist (plist-put arg-plist :exclude `(quote ,exdates))))
1506 (when rdates
1507 (setq arg-plist (plist-put arg-plist :include `(quote ,rdates))))
1508 (when dtstart
1509 (setq arg-plist (plist-put arg-plist :start `(quote ,dtstart))))
1510 (when dur-value
1511 (setq arg-plist (plist-put arg-plist :duration `(quote ,dur-value))))
1512 (when rrule
1513 ;; TODO: make this prettier to look at?
1514 (setq arg-plist (append (list :rule `(quote ,rrule)) arg-plist)))
1515 ;; TODO: timezones??
1516
1517 (setq arg-plist (cons 'diary-rrule arg-plist))
1518 (string-trim ; removing trailing \n added by pp
1519 (concat diary-sexp-entry-symbol
1520 (with-output-to-string (pp arg-plist)))))))
1521
1522;; This function puts all of the above together to format individual
1523;; iCalendar components as diary entries. The final formatting is done
1524;; by the appropriate skeleton command for the component, or by
1525;; `di:-format-vevent-legacy' if the legacy format string variables from
1526;; icalendar.el are set.
1527(defun di:format-entry (component index &optional nonmarking)
1528 "Format an iCalendar component for the diary.
1529
1530COMPONENT should be an `icalendar-vevent', `icalendar-vtodo', or
1531`icalendar-vjournal'. INDEX should be an index into the calendar where
1532COMPONENT occurs, as returned by `icalendar-parse-and-index'.
1533
1534Depending on the type of COMPONENT, the body will be formatted by one of:
1535`diary-icalendar-vevent-format-function'
1536`diary-icalendar-vtodo-format-function'
1537`diary-icalendar-vjournal-format-function'
1538which see.
1539
1540The variable `ical-nonmarking' will be bound to the value of NONMARKING in
1541the relevant skeleton command. If it is non-nil, the user requested the
1542entry to be nonmarking.
1543
1544Returns a string containing the diary entry."
1545 (ical:with-component component
1546 ((ical:attach :all attach-nodes)
1547 (ical:attendee :all attendee-nodes)
1548 (ical:categories :all categories-nodes)
1549 (ical:class :value access)
1550 (ical:comment :all comment-nodes)
1551 (ical:completed :value completed-dt)
1552 (ical:contact :all contact-nodes)
1553 (ical:description :value description)
1554 ;; in `icalendar-vjournal', multiple `icalendar-description'
1555 ;; nodes are allowed:
1556 (ical:description :all description-nodes)
1557 (ical:dtend :first dtend-node :value dtend)
1558 (ical:dtstart :first dtstart-node :value dtstart)
1559 (ical:duration :value duration)
1560 (ical:due :first due-node :value due-dt)
1561 (ical:geo :value geo)
1562 (ical:location :value location)
1563 (ical:organizer :first organizer-node ; for skeleton formatting
1564 :value organizer-addr) ; for legacy formatting
1565 (ical:percent-complete :value percent-complete)
1566 (ical:priority :value priority)
1567 (ical:rrule :value rrule)
1568 (ical:rdate :all rdate-nodes)
1569 (ical:status :value status)
1570 (ical:summary :value summary)
1571 (ical:transp :value transp)
1572 (ical:uid :value uid)
1573 (ical:url :value url))
1574 (let* ((is-recurring (or rdate-nodes rrule))
1575 (start-tz (when dtstart-node
1576 (ical:with-property dtstart-node
1577 ((ical:tzidparam :value tzid))
1578 (when tzid (ical:index-get index :tzid tzid)))))
1579 (start-tzname (when start-tz (icr:tzname-on dtstart start-tz)))
1580 (dtstart-local (ical:date/time-to-local dtstart))
1581 (due-tz (when due-node
1582 (ical:with-property due-node
1583 ((ical:tzidparam :value tzid))
1584 (when tzid (ical:index-get index :tzid tzid)))))
1585 (due-tzname (when due-tz (icr:tzname-on due-dt due-tz)))
1586 (dtend
1587 (cond (dtend dtend)
1588 ;; DTEND and DUE never occur in the same component,
1589 ;; so we alias dtend to due:
1590 (due-dt due-dt)
1591 (duration
1592 (ical:date/time-add-duration dtstart duration start-tz))))
1593 (dtend-local (ical:date/time-to-local dtend))
1594 (end-tz
1595 (cond (dtend-node
1596 (ical:with-property dtend-node
1597 ((ical:tzidparam :value tzid))
1598 (when tzid (ical:index-get index :tzid tzid))))
1599 (due-node due-tz)
1600 (duration start-tz)))
1601 (end-tzname (when end-tz (icr:tzname-on dtend end-tz)))
1602 (component-type (ical:ast-node-type component)))
1603 (dlet
1604 ;; We use "ical-" rather than "icalendar-" as prefix for these
1605 ;; vars because (a) it's shorter and (b) to avoid shadowing
1606 ;; any library symbols:
1607 ((ical-attachments
1608 (when attach-nodes
1609 (di:save-attachments-from attach-nodes uid)))
1610 (ical-attendees (mapcar #'di:format-attendee attendee-nodes))
1611 (ical-categories
1612 (mapcan
1613 (lambda (node)
1614 (mapcar #'ical:text-to-string (ical:ast-node-value node)))
1615 categories-nodes))
1616 (ical-access (when access (downcase access)))
1617 (ical-comments
1618 (mapcar
1619 (lambda (node) (ical:text-to-string (ical:ast-node-value node)))
1620 comment-nodes))
1621 (ical-contacts
1622 (mapcar
1623 (lambda (node) (ical:text-to-string (ical:ast-node-value node)))
1624 contact-nodes))
1625 (ical-completed
1626 (when completed-dt (di:format-date/time-as-local completed-dt)))
1627 (ical-description
1628 (if (eq 'icalendar-vjournal component-type)
1629 (mapconcat
1630 (lambda (node)
1631 (ical:trimp (ical:text-to-string (ical:ast-node-value node))))
1632 description-nodes
1633 "\n\n")
1634 (ical:trimp description)))
1635 (ical-start
1636 (when dtstart
1637 (if (bound-and-true-p ical-importing)
1638 (di:format-date/time-as-local dtstart start-tzname)
1639 (di:format-time-as-local dtstart start-tzname))))
1640 (ical-end
1641 (when dtend
1642 (if (bound-and-true-p ical-importing)
1643 (di:format-date/time-as-local dtend end-tzname)
1644 (di:format-time-as-local dtend end-tzname))))
1645 (ical-start-to-end
1646 (with-suppressed-warnings ((lexical date) (free-vars date))
1647 (cond
1648 ((not dtstart) nil)
1649 ((or (not dtend) (equal dtstart dtend))
1650 ;; without a distinct DTEND/DUE, same as start:
1651 (if (bound-and-true-p ical-importing)
1652 (di:format-date/time-as-local dtstart start-tzname)
1653 (di:format-time-as-local dtstart start-tzname)))
1654 ((and (bound-and-true-p ical-importing)
1655 (cl-typep dtstart 'ical:date)
1656 (cl-typep dtend 'ical:date))
1657 ;; Importing two dates:
1658 ;; %%(diary-block ...)
1659 (di:format-block-sexp
1660 dtstart
1661 ;; DTEND is an exclusive bound, while
1662 ;; diary-block needs an inclusive bound, so
1663 ;; subtract a day:
1664 (ical:date-add dtend :day -1)))
1665 ((and (bound-and-true-p ical-importing)
1666 (equal (ical:date/time-to-date dtstart-local)
1667 (ical:date/time-to-date dtend-local)))
1668 ;; Importing, start and end times on same day:
1669 ;; DATE HH:MM-HH:MM
1670 (di:format-time-range dtstart-local dtend-local))
1671 ((bound-and-true-p ical-importing)
1672 ;; Importing at least one date-time, on different days:
1673 ;; %%(diary-time-block :start ... :end ...)
1674 (di:format-time-block-sexp dtstart-local dtend-local))
1675 ((and (boundp 'date) ; bound when displaying diary
1676 (cl-typep dtstart-local 'ical:date-time)
1677 (cl-typep dtend-local 'ical:date-time)
1678 (equal date (ical:date-time-to-date dtstart-local))
1679 (equal date (ical:date-time-to-date dtend-local)))
1680 ;; Displaying, start and end times on the day displayed:
1681 ;; HH:MM-HH:MM
1682 (di:format-time-range dtstart-local dtend-local t))
1683 ((and (boundp 'date) ; bound when displaying diary
1684 (cl-typep dtstart-local 'ical:date-time)
1685 (cl-typep dtend-local 'ical:date-time))
1686 ;; Displaying, start and/or end time on other days:
1687 ;; HH:MM-HH:MM for just the times on `date'
1688 (di:format-time-range
1689 (ical:date/time-max dtstart-local
1690 (ical:make-date-time
1691 :year (calendar-extract-year date)
1692 :month (calendar-extract-month date)
1693 :day (calendar-extract-day date)
1694 :hour 0 :minute 0 :second 0
1695 :zone
1696 (decoded-time-zone dtstart-local)))
1697 (ical:date/time-min dtend-local
1698 (ical:make-date-time
1699 :year (calendar-extract-year date)
1700 :month (calendar-extract-month date)
1701 :day (calendar-extract-day date)
1702 :hour 23 :minute 59 :second 59
1703 :zone
1704 (decoded-time-zone dtend-local)))))
1705 (t
1706 ;; That's all the cases we care about here.
1707 nil))))
1708 (ical-due
1709 (when (eq component-type 'ical:vtodo)
1710 (if due-node
1711 (di:format-date/time-as-local due-dt due-tzname)
1712 ;; here we use start-tzname because due/dtend is calculated from
1713 ;; dtstart, not its own node with a tzid:
1714 (di:format-date/time-as-local dtend start-tzname))))
1715 (ical-work-time-sexp
1716 (when (and dtstart due-dt (bound-and-true-p ical-importing))
1717 (di:format-time-block-sexp dtstart-local due-dt)))
1718 (ical-importing (bound-and-true-p ical-importing))
1719 (ical-location (or (ical:trimp location)
1720 (when geo (di:format-geo-coordinates geo))))
1721 (ical-nonmarking nonmarking)
1722 (ical-organizer (di:format-attendee organizer-node))
1723 (ical-percent-complete percent-complete)
1724 (ical-priority priority)
1725 (ical-rrule-sexp
1726 (when (and is-recurring (bound-and-true-p ical-importing))
1727 (di:format-rrule-sexp component)))
1728 (ical-status (when status (ical:trimp (downcase status))))
1729 (ical-summary (ical:trimp summary))
1730 (ical-transparency transp)
1731 (ical-uid (ical:trimp uid))
1732 (ical-url (ical:trimp url)))
1733 (with-temp-buffer
1734 (cl-case (ical:ast-node-type component)
1735 (ical:vevent
1736 (with-suppressed-warnings
1737 ((obsolete ical:import-format
1738 di:-use-legacy-vars-p
1739 di:-vevent-to-legacy-alist
1740 di:-format-vevent-legacy))
1741 ;; N.B. icalendar.el *only* imported VEVENT components
1742 (if (di:-use-legacy-vars-p)
1743 (if (functionp ical:import-format)
1744 (insert (funcall ical:import-format
1745 (di:-vevent-to-legacy-alist component)))
1746 (di:-format-vevent-legacy (or ical-rrule-sexp
1747 ical-start-to-end
1748 ical-start)
1749 ical-access ical-description
1750 ical-location organizer-addr
1751 ical-summary ical-status
1752 ical-url ical-uid))
1753 (funcall di:vevent-format-function component))))
1754 (ical:vtodo (funcall di:vtodo-format-function component))
1755 (ical:vjournal (funcall di:vjournal-format-function component)))
1756 (buffer-string))))))
1757
1758
1759;; Import to Diary
1760;;
1761;; `di:import-file' and `di:import-buffer' are the main user commands
1762;; for import. (These replace `icalendar-import-file' and
1763;; `icalendar-import-buffer' defined by icalendar.el, which are now
1764;; obsolete aliases to these commands.) `di:import-buffer-to-buffer' is
1765;; the function underlying these commands; it is the main import
1766;; function available for external Lisp code.
1767
1768;; `di:import-buffer-to-buffer' is the underlying function that formats
1769;; a complete `icalendar-vcalendar' as diary entries. This function runs
1770;; `di:post-entry-format-hook' after formatting each component as an
1771;; entry, and it runs `di:post-calendar-format-hook' after all entries
1772;; have been formatted. These hooks enable e.g. user review and
1773;; confirmation of each imported entry and of the whole imported
1774;; calendar.
1775(defvar di:post-entry-format-hook nil
1776 "Hook run after formatting a single iCalendar component as a diary entry.
1777
1778The functions in this hook are run by `diary-icalendar-import-buffer-to-buffer'
1779\(which see) after each component it formats. Each function will be
1780called in a (narrowed) buffer whose contents represent a single diary
1781entry.")
1782
1783(defvar di:post-calendar-format-hook nil
1784 "Hook run after formatting a complete `icalendar-vcalendar' as diary entries.
1785
1786The functions in this hook are run by `diary-icalendar-import-buffer-to-buffer'
1787\(which see) after formatting all the diary entries created from the
1788calendar. Each function will be called in a buffer containing all the
1789diary entries.")
1790
1791(defun di:sort-by-start-ascending (c1 c2)
1792 "Sort iCalendar component C1 before C2 if C1 starts strictly before C2.
1793Components with no start date/time are sorted after components that do."
1794 (let ((c1start (ical:with-property-of c1 'ical:dtstart nil value))
1795 (c2start (ical:with-property-of c2 'ical:dtstart nil value)))
1796 (cond ((and c1start c2start)
1797 (ical:date/time< c1start c2start))
1798 ;; order anything with a start before anything without:
1799 (c1start t)
1800 (c2start nil)
1801 ;; otherwise they can stay as-is:
1802 (t t))))
1803
1804(defcustom di:import-comparison-function #'di:sort-by-start-ascending
1805 "Comparison function for sorting imported iCalendar components.
1806See the :lessp argument of `sort' for more information."
1807 :version "31.1"
1808 :type '(radio (function-item di:sort-by-start-ascending)
1809 (function :tag "Other comparison function")))
1810
1811(defun di:import-buffer-to-buffer (&optional all-nonmarking)
1812 "Format iCalendar data in current buffer as diary entries.
1813
1814This function parses the first iCalendar VCALENDAR in the current buffer
1815and formats its VEVENT, VJOURNAL, and VTODO components as diary entries.
1816It returns a new buffer containing those diary entries. The caller
1817should kill this buffer when it is no longer needed.
1818
1819If ALL-NONMARKING is non-nil, all diary entries will be non-marking.
1820
1821The list of components to import can be filtered by binding
1822`diary-icalendar-import-predicate'. After each component is formatted as
1823a diary entry, `diary-icalendar-post-entry-format-hook' is run in a (narrowed)
1824buffer containing that entry. After all components have been formatted,
1825`diary-icalendar-post-calendar-format-hook' is run in the (widened) buffer
1826containing all the entries.
1827
1828The formatting of imported entries depends on a number of
1829user-customizable variables, including: `diary-date-forms',
1830`calendar-date-style', `calendar-date-display-form' and customizations
1831in the `diary-icalendar' group."
1832 (unless (ical:contains-vcalendar-p (current-buffer))
1833 (di:signal-import-error (format "No VCALENDAR object in buffer %s"
1834 (buffer-name))))
1835 (save-excursion
1836 (goto-char (point-min))
1837 (let (vcalendar index)
1838 (ical:init-error-buffer)
1839 (let ((vcal/idx (ical:parse-and-index (current-buffer))))
1840 (when vcal/idx
1841 (setq vcalendar (car vcal/idx))
1842 (setq index (cadr vcal/idx))
1843 (let* ((import-buf (generate-new-buffer " *diary-import*"))
1844 (to-import
1845 (sort
1846 (seq-filter
1847 (lambda (c)
1848 (and (or (ical:vevent-component-p c)
1849 (ical:vjournal-component-p c)
1850 (ical:vtodo-component-p c))
1851 (funcall di:import-predicate c)))
1852 (ical:ast-node-children vcalendar))
1853 :lessp di:import-comparison-function
1854 :in-place t))
1855 ;; prevent point from being reset from window-point
1856 ;; when narrowed buffer is displayed for confirmation:
1857 (window-point-insertion-type t)
1858 ;; position at start of each entry:
1859 entry-start)
1860
1861 (with-current-buffer import-buf
1862 (dlet ((ical-importing t)) ; inform skeletons we're importing
1863 (dolist (component to-import)
1864 (setq entry-start (point))
1865 (insert (di:format-entry component index all-nonmarking))
1866 (with-restriction entry-start (point)
1867 (save-excursion
1868 (run-hooks 'di:post-entry-format-hook)))
1869 (unless (bolp) (insert "\n"))))
1870 (save-excursion
1871 (run-hooks 'di:post-calendar-format-hook))
1872 import-buf)))))))
1873
1874;; Internal variables needed by `di:-entry-import'. They are dynamically
1875;; bound in `di:import-buffer'.
1876(defvar di:-no-queries nil)
1877(defvar di:-entry-count nil)
1878
1879(defun di:-entry-import ()
1880 ;; Adds the formatted entry in the current restriction to the diary,
1881 ;; after getting confirmation from the user.
1882 ;; Used via `di:post-entry-format-hook' in `di:import-buffer', below.
1883 (unless di:-no-queries
1884 (display-buffer (current-buffer)))
1885 (when (or di:-no-queries
1886 (let ((help-form
1887 "Type y to add this entry to the diary, n to skip to next."))
1888 (di:y-or-n-or-edit-p "Add this entry to the diary?")))
1889 (ical:condition-case err
1890 (let* ((uid (save-excursion
1891 (goto-char (point-min))
1892 (when (re-search-forward di:uid-regexp nil t)
1893 (match-string 1))))
1894 (other-entry (di:find-entry-with-uid uid))
1895 (entry (buffer-string)))
1896 (if (and other-entry
1897 (not di:-no-queries)
1898 (y-or-n-p "Replace existing entry with same UID?"))
1899 (with-current-buffer (marker-buffer (car other-entry))
1900 (replace-region-contents
1901 (car other-entry) (cadr other-entry) entry))
1902 ;; Otherwise, diary-make-entry inserts the new entry at the end
1903 ;; of the main diary file:
1904 (diary-make-entry
1905 entry
1906 nil ; skeleton has already interpreted nonmarking
1907 nil ; use dynamic value of `diary-file'
1908 t ; skeleton responsible for final spaces
1909 t)) ; no need to show diary file while importing
1910 (when other-entry
1911 (set-marker (car other-entry) nil)
1912 (set-marker (cadr other-entry) nil))
1913 (cl-incf di:-entry-count)))))
1914
1915;;;###autoload
1916(defun di:import-buffer (&optional diary-filename quietly all-nonmarking)
1917 "Import iCalendar events from current buffer into diary.
1918
1919This function parses the first iCalendar VCALENDAR in the current buffer
1920and imports VEVENT, VJOURNAL, and VTODO components to the diary file
1921DIARY-FILENAME (default: `diary-file').
1922
1923For each entry, you are asked whether to add it to the diary unless
1924QUIETLY is non-nil. After all entries are imported, you are also asked
1925if you want to save the diary file unless QUIETLY is non-nil. When
1926called interactively, you are asked if you want to confirm each entry
1927individually; answer No to make QUIETLY non-nil.
1928
1929ALL-NONMARKING determines whether all diary events are created as
1930non-marking entries. When called interactively, you are asked whether
1931you want to make all entries non-marking.
1932
1933The formatting of imported entries in the diary depends on a number of
1934user-customizable variables. Before running this command for the first
1935time, you may especially wish to check the values of:
1936`diary-file'
1937`diary-date-forms'
1938`diary-date-insertion-form'
1939`calendar-date-style'
1940`calendar-date-display-form'
1941as well as variables in the customization group `diary-icalendar-import'."
1942 (interactive
1943 (list (read-file-name "Diary file: "
1944 (when diary-file (file-name-directory diary-file))
1945 (cons diary-file diary-included-files))
1946 (or di:always-import-quietly
1947 (not (y-or-n-p "Confirm entries individually?")))
1948 (y-or-n-p "Make all entries non-marking?")))
1949
1950 (let* ((diary-file diary-filename) ; dynamically bound for `di:-entry-import',
1951 (di:-entry-count 0) ; see above
1952 (di:-no-queries quietly) ;
1953 (di:post-entry-format-hook
1954 (append di:post-entry-format-hook (list #'di:-entry-import)))
1955 (diary-buffer (or (find-buffer-visiting diary-filename)
1956 (find-file-noselect diary-filename)))
1957 import-buffer)
1958 (unwind-protect
1959 (setq import-buffer (di:import-buffer-to-buffer all-nonmarking))
1960 (when (bufferp import-buffer)
1961 (kill-buffer import-buffer)))
1962 (display-buffer diary-buffer)
1963 (when (or quietly
1964 (y-or-n-p (format "%d entries imported. Save diary file?"
1965 di:-entry-count)))
1966 (with-current-buffer diary-buffer
1967 (goto-char (point-max))
1968 (save-buffer)))))
1969
1970;;;###autoload
1971(defun di:import-file (filename &optional diary-filename quietly nonmarking)
1972 "Import iCalendar diary entries from FILENAME into DIARY-FILENAME.
1973
1974This function parses the first iCalendar VCALENDAR in FILENAME and
1975imports VEVENT, VJOURNAL, and VTODO components to the diary
1976DIARY-FILENAME (default: `diary-file').
1977
1978For each entry, you are asked whether to add it to the diary unless
1979QUIETLY is non-nil. After all entries are imported, you are also asked
1980if you want to save the diary file unless QUIETLY is non-nil. When
1981called interactively, you are asked if you want to confirm each entry
1982individually; answer No to make QUIETLY non-nil.
1983
1984NONMARKING determines whether all diary events are created as
1985non-marking entries. When called interactively, you are asked whether
1986you want to make all entries non-marking.
1987
1988The formatting of imported entries in the diary depends on a number of
1989user-customizable variables. Before running this command for the first
1990time, you may especially wish to check the values of:
1991`diary-file'
1992`diary-date-forms'
1993`diary-date-insertion-form'
1994`calendar-date-style'
1995`calendar-date-display-form'
1996as well as variables in the customization group `diary-icalendar-import'."
1997 (interactive
1998 (list (read-file-name "iCalendar file: " nil nil 'confirm)
1999 (read-file-name "Diary file: "
2000 (when diary-file (file-name-directory diary-file))
2001 (cons diary-file diary-included-files))
2002 (or di:always-import-quietly
2003 (not (y-or-n-p "Confirm entries individually?")))
2004 (y-or-n-p "Make all entries non-marking?")))
2005 (let ((parse-buf (ical:find-unfolded-buffer-visiting filename)))
2006 (unless parse-buf
2007 (ical:condition-case err
2008 (setq parse-buf
2009 (ical:unfolded-buffer-from-file (expand-file-name filename)))))
2010 ;; Hand off to `di:import-buffer' for the actual import:
2011 (if parse-buf
2012 (with-current-buffer parse-buf
2013 (di:import-buffer diary-filename quietly nonmarking))
2014 ;; If we get here, we weren't able to open the file for parsing:
2015 (warn "Unable to open file %s; see %s"
2016 filename (buffer-name (ical:error-buffer))))))
2017
2018;; Some simple support for viewing iCalendar data in MIME message
2019;; parts. Mail readers may want to build their own viewer using the
2020;; import functions above, but this is a good starting point:
2021(defun di:mailcap-viewer ()
2022 "View iCalendar data in the current message part as diary entries.
2023
2024This function is a suitable viewer for text/calendar parts in MIME
2025messages, such as email attachments. To use this function as a viewer,
2026customize the variable `mailcap-user-mime-data' and add an entry
2027containing this function for the MIME type \"text/calendar\".
2028
2029To extend the behavior of this function, see
2030`diary-icalendar-after-mailcap-viewer-hook'."
2031 (let ((entries-buf (diary-icalendar-import-buffer-to-buffer)))
2032 (unwind-protect
2033 (progn
2034 ;; Since this is already a temporary viewer buffer, we replace
2035 ;; its contents with the imported entries, so we can (a) keep
2036 ;; the window configuration setup by the calling mailcap code
2037 ;; and (b) already kill the import buffer here.
2038 (erase-buffer)
2039 (insert-buffer-substring entries-buf)
2040 (diary-mode)
2041 (run-hooks di:after-mailcap-viewer-hook))
2042 (kill-buffer entries-buf))))
2043
2044
2045;; Export
2046
2047;;; Error handling
2048(define-error 'ical:diary-export-error "Unable to export diary data" 'ical:error)
2049
2050(cl-defun di:signal-export-error (msg &key (diary-buffer (current-buffer))
2051 (position (point))
2052 line
2053 (severity 2))
2054 (let ((err-data
2055 (list :message msg
2056 :buffer diary-buffer
2057 :position position
2058 :line line
2059 :severity severity)))
2060 (signal 'ical:diary-export-error err-data)))
2061
2062;;; Export utility functions
2063(defun di:parse-attendees-and-organizer ()
2064 "Parse `icalendar-attendee' and `icalendar-organizer' nodes from entry.
2065
2066Searches the entry in the current restriction for addresses matching
2067`diary-icalendar-address-regexp'. If an address is found on a
2068line that also matches `diary-icalendar-organizer-regexp', it will be
2069parsed as an `icalendar-organizer' node, or otherwise as an
2070`icalendar-attendee'. Returns the list of nodes for all addresses found."
2071 (goto-char (point-min))
2072 (let (attendees organizer)
2073 (while (re-search-forward di:address-regexp nil t)
2074 (let ((addr (match-string 1))
2075 (cn (match-string 2)))
2076 (unless (string-match ":" addr) ; URI scheme already present
2077 (setq addr (concat "mailto:" addr)))
2078 (when cn
2079 (setq cn (ical:trimp cn)))
2080 (if (string-match di:organizer-regexp
2081 (buffer-substring (line-beginning-position)
2082 (line-end-position)))
2083 (setq organizer
2084 (ical:make-property ical:organizer addr (ical:cnparam cn)))
2085 (push (ical:make-property ical:attendee addr (ical:cnparam cn))
2086 attendees))))
2087 (if organizer
2088 (cons organizer attendees)
2089 attendees)))
2090
2091(defun di:parse-location ()
2092 "Parse `icalendar-location' node from entry.
2093
2094Searches the entry in the current restriction for a location matching
2095`diary-icalendar-location-regexp'. If a location is found, it will be
2096parsed as an `icalendar-location' node. Returns a list containing just
2097this node, or nil."
2098 (goto-char (point-min))
2099 (when (and di:location-regexp
2100 (re-search-forward di:location-regexp nil t))
2101 (ical:make-property ical:location (ical:trimp (match-string 1)))))
2102
2103(defun di:parse-class ()
2104 "Parse `icalendar-class' node from entry.
2105
2106Searches the entry in the current restriction for an access
2107classification matching `diary-icalendar-class-regexp'. If a
2108classification is found, it will be parsed as an `icalendar-class'
2109node. Return this node, or nil."
2110 (goto-char (point-min))
2111 (when (and di:class-regexp
2112 (re-search-forward di:class-regexp nil t))
2113 (ical:make-property ical:class
2114 (upcase (string-trim (match-string 1))))))
2115
2116(defun di:parse-status ()
2117 "Parse `icalendar-status' node from entry.
2118
2119Searches the entry in the current restriction for a status matching
2120`diary-icalendar-status-regexp'. If a status is found, it will be parsed
2121as an `icalendar-status' node. Return this node, or nil."
2122 (goto-char (point-min))
2123 (when (and di:status-regexp
2124 (re-search-forward di:status-regexp nil t))
2125 (ical:make-property ical:status
2126 (upcase (string-trim (match-string 1))))))
2127
2128(defun di:parse-url ()
2129 "Parse `icalendar-url' node from entry.
2130
2131Searches the entry in the current restriction for an URL matching
2132`diary-icalendar-url-regexp'. If an URL is found, it will be parsed as an
2133`icalendar-url' node. Return this node, or nil."
2134 (goto-char (point-min))
2135 (when (and di:url-regexp
2136 (re-search-forward di:url-regexp nil t))
2137 (ical:make-property ical:url (ical:trimp (match-string 1)))))
2138
2139(defun di:parse-uid ()
2140 "Parse `icalendar-uid' node from entry.
2141
2142Searches the entry in the current restriction for a UID matching
2143`diary-icalendar-uid-regexp'. If a UID is found, it will be parsed as an
2144`icalendar-uid' node. Return this node, or nil."
2145 (goto-char (point-min))
2146 (when (and di:uid-regexp
2147 (re-search-forward di:uid-regexp nil t))
2148 (ical:make-property ical:uid (ical:trimp (match-string 1)))))
2149
2150(defun di:parse-summary-and-description ()
2151 "Parse summary and description nodes from current restriction.
2152
2153When `diary-icalendar-summary-regexp' or
2154`diary-icalendar-description-regexp' are non-nil, and the entry matches
2155them, the matches will be used to generate the summary and description.
2156
2157Otherwise, the first line of the entry (after any nonmarking symbol and
2158date and time specification) is used as the summary. The description is
2159the full body of the entry, excluding the nonmarking symbol, date and
2160time, but including the summary.
2161
2162Returns a list containing an `icalendar-summary' node and
2163`icalendar-description' node, or nil."
2164 (goto-char (point-min))
2165 (let (summary description)
2166 (when (and di:summary-regexp
2167 (re-search-forward di:summary-regexp nil t))
2168 (setq summary (match-string 1)))
2169 (goto-char (point-min))
2170 (when (and di:description-regexp
2171 (re-search-forward di:description-regexp nil t))
2172 (setq description (match-string 1)))
2173 ;; Fall back to using first line and entire entry:
2174 (goto-char (point-min))
2175 (while (looking-at-p "[[:space:]]")
2176 (forward-char))
2177 (unless summary
2178 (setq summary (buffer-substring (point) (line-end-position))))
2179 (unless description
2180 (setq description (buffer-substring (point) (point-max))))
2181 ;; Remove any indentation on subsequent lines from description:
2182 (setq description (replace-regexp-in-string "^[[:space:]]+" "" description))
2183
2184 (list (ical:make-property ical:summary summary)
2185 (ical:make-property ical:description description))))
2186
2187(defun di:parse-entry-type ()
2188 "Return the type symbol for the component type used to export an entry.
2189
2190Default is `icalendar-vevent'. If the entry is nonmarking and
2191`diary-icalendar-export-nonmarking-as-vjournal' is non-nil,
2192`icalendar-vjournal' is returned. If `diary-icalendar-todo-regexp' is
2193non-nil and the entry matches it, `icalendar-vtodo' is returned.
2194
2195If the entry is nonmarking and `diary-icalendar-export-nonmarking-entries'
2196is nil, nil is returned, indicating that the entry should not be
2197exported."
2198 (let (type)
2199 (goto-char (point-min))
2200 (unless (and (looking-at-p diary-nonmarking-symbol)
2201 (not di:export-nonmarking-entries))
2202 (setq type 'ical:vevent)
2203 (when (and (looking-at-p diary-nonmarking-symbol)
2204 di:export-nonmarking-as-vjournal)
2205 (setq type 'ical:vjournal))
2206 (when (and di:todo-regexp (re-search-forward di:todo-regexp nil t))
2207 (setq type 'ical:vtodo)))
2208 type))
2209
2210(defun di:parse-transparency (type)
2211 "Return the iCalendar time transparency of an entry.
2212
2213TYPE should be the type symbol for the component to be exported, as
2214returned by `diary-icalendar-parse-entry-type'. If the entry is
2215non-marking (i.e., begins with `diary-nonmarking-symbol'), and it is to
2216be exported as an `icalendar-vevent' (according to TYPE), then this
2217function returns a list containing the appropriate `icalendar-transp'
2218property node to mark the event as transparent, and moves the current
2219restriction past the non-marking symbol. Otherwise it returns nil."
2220 (save-excursion
2221 (goto-char (point-min))
2222 (when (and (eq type 'ical:vevent)
2223 (re-search-forward (concat "^" diary-nonmarking-symbol)
2224 (line-end-position) t))
2225 (narrow-to-region (point) (point-max))
2226 (list
2227 (ical:make-property ical:transp "TRANSPARENT")))))
2228
2229;; TODO: move to diary-lib?
2230(defun di:parse-date-form ()
2231 "Parse a date matching `diary-date-forms' on the current line.
2232
2233If a date is found, moves the current restriction past the end of the
2234date and returns a list (MONTH DAY YEAR), where each value is an integer
2235or t if the date is generic in that unit. Otherwise returns nil."
2236 (goto-char (point-min))
2237 (catch 'date
2238 (let (date-regexp backup)
2239 (dolist (date-sexp diary-date-forms)
2240 (when (eq 'backup (car date-sexp))
2241 (setq date-sexp (cdr date-sexp))
2242 (setq backup t))
2243 (setq date-regexp (di:date-form-to-regexp date-sexp))
2244 (when backup (beginning-of-line))
2245 (when (let ((case-fold-search t))
2246 (re-search-forward date-regexp nil t))
2247 (let ((year
2248 (let ((match (match-string 1)))
2249 (if (or (null match) (equal match "*"))
2250 t
2251 (if (and diary-abbreviated-year-flag (length= match 2))
2252 ;; from diary-lib.el:
2253 ;; Add 2-digit year to current century.
2254 ;; If more than 50 years in the future,
2255 ;; assume last century. If more than 50
2256 ;; years in the past, assume next century.
2257 (let* ((current-y
2258 (calendar-extract-year (calendar-current-date)))
2259 (y (+ (string-to-number match)
2260 ;; Current century, eg 2000.
2261 (* 100 (/ current-y 100))))
2262 (offset (- y current-y)))
2263 (cond ((> offset 50)
2264 (- y 100))
2265 ((< offset -50)
2266 (+ y 100))
2267 (t y)))
2268 (string-to-number match)))))
2269 (month
2270 (let ((month-num (match-string 2))
2271 (month-name (match-string 4)))
2272 (cond ((or (equal month-name "*") (equal month-num "*")) t)
2273 (month-num (string-to-number month-num))
2274 (month-name
2275 (alist-get
2276 (capitalize month-name)
2277 (calendar-make-alist
2278 calendar-month-name-array
2279 1 nil
2280 calendar-month-abbrev-array
2281 (mapcar (lambda (e) (format "%s." e))
2282 calendar-month-abbrev-array))
2283 nil nil #'equal)))))
2284 (day
2285 (let ((day-num (match-string 3))
2286 (day-name (match-string 5)))
2287 (cond
2288 ;; We don't care about the day name here, unless it
2289 ;; is "*", since it won't help us identify a day of
2290 ;; the month. Weekly entries under a weekday name
2291 ;; are parsed by `di:parse-weekday-name', below.
2292 ((or (equal day-name "*") (equal day-num "*")) t)
2293 (day-num (string-to-number day-num))))))
2294 (when (and year month day)
2295 (narrow-to-region (match-end 0) (point-max))
2296 (throw 'date (list month day year)))))))))
2297
2298(defun di:date-form-to-regexp (date-sexp)
2299 "Convert DATE-SEXP to a regular expression.
2300
2301DATE-SEXP should be an S-expression in the variables `year', `month',
2302`day', `monthname', and `dayname', as found e.g. in `diary-date-forms'.
2303The returned regular expression matches dates of this form, including
2304generic dates specified with \"*\", and abbreviated and long-form month
2305and day names (based on `calendar-month-name-array' and
2306`calendar-month-abbrev-array', and similarly for day names). The match
2307groups contain the following data:
2308
2309Group 1: the 2-4 digit year, or a literal *
2310Group 2: the 1-2 digit month number, or a literal *
2311Group 3: the 1-2 digit day number, or a literal *
2312Group 4: the (long-form or abbreviated) month name, or a literal *
2313Group 5: the (long-form or abbreviated) day name, or a literal *"
2314 (when (eq 'backup (car date-sexp))
2315 (setq date-sexp (cdr date-sexp)))
2316 (let ((month-names-regexp
2317 (rx
2318 (group-n 4
2319 (or (regexp (diary-name-pattern calendar-month-name-array
2320 calendar-month-abbrev-array))
2321 "*"))))
2322 (day-names-regexp
2323 (rx
2324 (group-n 5
2325 (or (regexp (diary-name-pattern calendar-day-name-array
2326 calendar-day-abbrev-array))
2327 "*"))))
2328 date-regexp)
2329 (calendar-dlet
2330 ((prefix (rx line-start
2331 (zero-or-one (regexp diary-nonmarking-symbol))))
2332 (year (rx (group-n 1 (or (** 2 4 digit) "*"))))
2333 (month (rx (group-n 2 (or (** 1 2 digit) "*"))))
2334 (day (rx (group-n 3 (or (** 1 2 digit) "*"))))
2335 (monthname month-names-regexp)
2336 (dayname day-names-regexp))
2337 (setq date-regexp (apply #'concat (cons prefix (mapcar #'eval date-sexp)))))
2338 date-regexp))
2339
2340(defun di:parse-weekday-name ()
2341 "Parse a weekday name on the current line.
2342
2343The day name must appear in `calendar-day-name-array' or
2344`calendar-day-abbrev-array'. If a day name is found, move the current
2345restriction past it, and return a day number between 0 (=Sunday) and
23466 (=Saturday). Otherwise, return nil."
2347 (goto-char (point-min))
2348 (let ((day-names-regexp
2349 (rx line-start
2350 (zero-or-one (regexp diary-nonmarking-symbol))
2351 (group-n 1
2352 (regexp (diary-name-pattern calendar-day-name-array
2353 calendar-day-abbrev-array))))))
2354 (when (re-search-forward day-names-regexp (line-end-position) t)
2355 (let ((day-name (capitalize (match-string 1))))
2356 (narrow-to-region (match-end 0) (point-max))
2357 (alist-get
2358 day-name
2359 (calendar-make-alist calendar-day-name-array 0 nil
2360 calendar-day-abbrev-array
2361 (mapcar (lambda (e) (format "%s." e))
2362 calendar-day-abbrev-array))
2363 nil nil #'equal)))))
2364
2365(defun di:weekday-to-recurrence (weekday)
2366 "Convert WEEKDAY to a WEEKLY iCalendar recurrence rule.
2367
2368WEEKDAY must be an integer between 0 (=Sunday) and 6 (=Saturday).
2369Returns a list (START RRULE), with START being an `icalendar-dtstart'
2370property and RRULE an `icalendar-rrule'."
2371 (let ((dtstart (calendar-nth-named-day 1 weekday 1 di:recurring-start-year))
2372 (rrule `((FREQ WEEKLY)
2373 (BYDAY (,weekday)))))
2374 (list (ical:make-property ical:dtstart dtstart)
2375 (ical:make-property ical:rrule rrule))))
2376
2377;; TODO: give this value to diary-time-regexp?
2378(defconst di:time-regexp
2379 (rx-let ((hours (or (seq (any "0-2") (any "0-9"))
2380 (any "0-9")))
2381 (minutes (seq (any "0-5") (any "0-9")))
2382 (am/pm (seq (any "ap") "m"))) ;; am, pm
2383 (rx
2384 (group-n 1 ;; START
2385 (group-n 11 hours) ;; start hour
2386 (or
2387 ;; 10:00 or 10h00:
2388 (seq (or ":" "h") (group-n 12 minutes) (opt (group-n 13 am/pm)))
2389 ;; 10.00h or 10.00am: (a bare "10.00" should not match)
2390 (seq "." (group-n 12 minutes) (or (group-n 13 am/pm) "h"))
2391 ;; 10am
2392 (group-n 13 am/pm)
2393 ;; 10h
2394 "h"))
2395 (zero-or-one
2396 (one-or-more "-")
2397 (group-n 2 ;; END
2398 (group-n 21 hours) ;; end hour
2399 (or
2400 ;; 10:00 or 10h00:
2401 (seq (or ":" "h") (group-n 22 minutes) (opt (group-n 23 am/pm)))
2402 ;; 10.00h or 10.00am:
2403 (seq "." (group-n 22 minutes) (or "h" (group-n 23 am/pm)))
2404 ;; 10am
2405 (group-n 23 am/pm)
2406 ;; 10h
2407 "h")))
2408 (one-or-more space)))
2409 "Regular expression to match diary appointment times.
2410
2411Accepted time formats look like e.g.:
2412 9AM 9:00 09:00 9h 9h00 9.00am 9.00h
2413 9PM 9:00pm 21:00 21h00 21.00pm 21.00h
2414 9AM-1PM 09:00-13:00
2415
2416Group 1 matches the start time:
2417 Group 11 matches the hours digits
2418 Group 12 matches the minutes digits
2419 Group 13 matches an AM/PM specification
2420
2421Group 2 matches the end time:
2422 Group 21 matches the hours digits
2423 Group 22 matches the minutes digits
2424 Group 23 matches an AM/PM specification")
2425
2426(defun di:parse-time ()
2427 "Parse diary time string in the current restriction.
2428
2429If a time specification is found, move the current restriction past it,
2430and return a list (START END), where START and END are decoded-time
2431values containing the hours and minutes slots parsed from the time
2432specification. END may be nil if no end time was specified."
2433 (goto-char (point-min))
2434 (let ((regexp di:time-regexp)
2435 (case-fold-search t))
2436 (when di:export-linewise
2437 ;; In this case, only look for a time following whitespace,
2438 ;; at the beginning of a continuation line of the full entry:
2439 (setq regexp (concat "^[[:space:]]+" di:time-regexp)))
2440
2441 (when (re-search-forward regexp (line-end-position) t)
2442 (let* ((start-hh (string-to-number (match-string 11)))
2443 (start-am/pm (when (match-string 13)
2444 (upcase (match-string 13))))
2445 (start-hours (if (and (equal start-am/pm "PM") (< start-hh 12))
2446 (+ 12 start-hh)
2447 start-hh))
2448 (start-minutes (string-to-number (or (match-string 12) "0")))
2449 (start
2450 (when (and start-hours start-minutes)
2451 (make-decoded-time :hour start-hours
2452 :minute start-minutes
2453 :second 0)))
2454 (end-hh (when (match-string 21)
2455 (string-to-number (match-string 21))))
2456 (end-am/pm (when (match-string 23)
2457 (upcase (match-string 23))))
2458 (end-hours (if (and end-hh (equal end-am/pm "PM") (< end-hh 12))
2459 (+ 12 end-hh)
2460 end-hh))
2461 (end-minutes (when end-hours
2462 (string-to-number (or (match-string 22) "0"))))
2463 (end (when (and end-hours end-minutes)
2464 (make-decoded-time :hour end-hours
2465 :minute end-minutes
2466 :second 0))))
2467 (narrow-to-region (match-end 0) (point-max))
2468 ;; Return the times:
2469 (list start end)))))
2470
2471(defun di:convert-time-via-strategy (dt &optional vtimezone)
2472 "Reinterpret the local time DT per the time zone export strategy.
2473
2474The export strategy is determined by
2475`diary-icalendar-time-zone-export-strategy', which see.
2476
2477DT may be an `icalendar-date' or `icalendar-date-time'. If it is a
2478date, it is returned unmodified. If it is a date-time, depending on the
2479strategy and any existing zone information in DT, it will be converted
2480to a correct local, UTC, or floating time. VTIMEZONE should be the
2481`icalendar-vtimezone' which defines the local time zone, if the time
2482zone export strategy requires it."
2483 (cl-typecase dt
2484 (ical:date dt)
2485 (ical:date-time
2486 (cond
2487 ((or (eq 'local di:time-zone-export-strategy)
2488 (listp di:time-zone-export-strategy))
2489 (unless (ical:vtimezone-component-p vtimezone)
2490 (di:signal-export-error
2491 (format
2492 "%s time export strategy requires a time zone definition;\n%s"
2493 (if (eq 'local di:time-zone-export-strategy) "`local'" "list-based")
2494 (concat
2495 "check the value of `diary-icalendar-time-zone-export-strategy'\n"
2496 "and the output of `calendar-current-time-zone'"))))
2497 (if (decoded-time-zone dt)
2498 (icr:tz-decode-time (encode-time dt) vtimezone)
2499 (icr:tz-set-zone dt vtimezone :error)))
2500 ((eq 'to-utc di:time-zone-export-strategy)
2501 (decode-time (encode-time dt) t))
2502 ((eq 'floating di:time-zone-export-strategy)
2503 (setf (decoded-time-zone dt) nil)
2504 dt)))))
2505
2506(defun di:parse-sexp ()
2507 "Parse a diary S-expression at the beginning of the current restriction.
2508
2509The S-expression must appear at the start of line, immediately after
2510`diary-sexp-entry-symbol'. If an S-expression is found, move the
2511current restriction past it, and return the S-expression. Otherwise,
2512return nil."
2513 (goto-char (point-min))
2514 (let ((regexp (rx line-start
2515 (regexp diary-sexp-entry-symbol))))
2516 (when (re-search-forward regexp (line-end-position) t)
2517 (let ((sexp (read (current-buffer))))
2518 (narrow-to-region (point) (point-max))
2519 sexp))))
2520
2521(defun di:anniversary-sexp-to-recurrence (sexp)
2522 "Convert `diary-anniversary' SEXP to `icalendar-dtstart' and `icalendar-rrule'.
2523Returns a pair of nodes (START RRULE)."
2524 (let* ((d1 (nth 1 sexp))
2525 (d2 (nth 2 sexp))
2526 (d3 (nth 3 sexp))
2527 (dtstart (diary-make-date d1 d2 (or d3 di:recurring-start-year)))
2528 (rrule '((FREQ YEARLY))))
2529 (list
2530 (ical:make-property ical:dtstart dtstart (ical:valuetypeparam 'ical:date))
2531 (ical:make-property ical:rrule rrule))))
2532
2533(defun di:block-sexp-to-recurrence (sexp)
2534 "Convert `diary-block' SEXP to `icalendar-dtstart' and `icalendar-rrule' nodes.
2535Returns a pair of nodes (START RRULE)."
2536 (let* ((dtstart (diary-make-date (nth 1 sexp) (nth 2 sexp) (nth 3 sexp)))
2537 (end (diary-make-date (nth 4 sexp) (nth 5 sexp) (nth 6 sexp)))
2538 (rrule `((FREQ DAILY)
2539 (UNTIL ,end))))
2540 (list (ical:make-property ical:dtstart dtstart
2541 (ical:valuetypeparam 'ical:date))
2542 (ical:make-property ical:rrule rrule))))
2543
2544(defun di:time-block-sexp-to-start-end (sexp &optional vtimezone)
2545 "Convert `diary-time-block' SEXP to `icalendar-dtstart' and `icalendar-dtend'.
2546Returns a pair of nodes (START END).
2547
2548VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in
2549SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating
2550times according to `diary-icalendar-time-zone-export-strategy'."
2551 (let* ((start (plist-get sexp :start))
2552 (dtstart (di:convert-time-via-strategy start vtimezone))
2553 (end (plist-get sexp :end))
2554 (dtend (di:convert-time-via-strategy end vtimezone))
2555 (tzid (ical:with-property-of vtimezone 'ical:tzid)))
2556 (list (ical:make-property ical:dtstart dtstart (ical:tzidparam tzid))
2557 (ical:make-property ical:dtend dtend (ical:tzidparam tzid)))))
2558
2559(defun di:cyclic-sexp-to-recurrence (sexp)
2560 "Convert `diary-cyclic' SEXP to `icalendar-dtstart' and `icalendar-rrule'.
2561Returns a pair of nodes (START RRULE)."
2562 (let* ((ndays (nth 1 sexp))
2563 (dtstart (diary-make-date (nth 2 sexp) (nth 3 sexp) (nth 4 sexp)))
2564 (rrule `((FREQ DAILY)
2565 (INTERVAL ,ndays))))
2566 (list
2567 (ical:make-property ical:dtstart dtstart (ical:valuetypeparam 'ical:date))
2568 (ical:make-property ical:rrule rrule))))
2569
2570(defun di:float-sexp-to-recurrence (sexp)
2571 "Convert `diary-float' SEXP to `icalendar-dtstart' and `icalendar-rrule'.
2572Returns a pair of nodes (START RRULE)."
2573 (let* ((month-exp (nth 1 sexp))
2574 (months (cond ((eq month-exp t) nil) ; don't add a BYMONTH clause
2575 ((integerp month-exp) (list month-exp))
2576 ((and (listp month-exp) (eq 'quote (car month-exp)))
2577 (eval month-exp nil)) ; unquote a literal list of ints
2578 (t month-exp)))
2579 (_ (unless (seq-every-p #'integerp months)
2580 (di:signal-export-error
2581 (format "Malformed month(s) in `diary-float' S-expression:\n%s"
2582 sexp))))
2583 (dow (nth 2 sexp))
2584 (n (nth 3 sexp))
2585 (day (or (nth 4 sexp)
2586 (if (< 0 n) 1
2587 'last))) ; = "last day of the month" for any month
2588 ;; Calculate the offset within the month from day, n:
2589 (offset
2590 (cond ((eq day 'last) n)
2591 ((and (< 0 day) (< 0 n))
2592 ;; In this case, to get the offset relative to
2593 ;; the start of the month, we need to add to n
2594 ;; the number of weeks in the month before day:
2595 ;; e.g. if day = 8, n = 2, then we are looking
2596 ;; for the second DOW after the 8th of the
2597 ;; month, which is the 3rd DOW after the 1st of
2598 ;; the month
2599 (+ n (/ (1- day) 7)))
2600 ((and (< 0 day) (< n 0) (< day (* 7 (abs n))))
2601 ;; In this case, we need to cross into the
2602 ;; previous month and adjust the offset
2603 ;; accordingly to reflect the correct number of
2604 ;; weeks before the end of the month.
2605 ;; e.g. if day = 15, n = -3, we're looking for the
2606 ;; 3rd DOW before the 15th of the month,
2607 ;; which is the 1st DOW "before" the end of the
2608 ;; previous month (where "before" is inclusive,
2609 ;; e.g offset = -1 will work when DOW is the last
2610 ;; day of the month)
2611 (when months
2612 (setq months
2613 (sort
2614 :in-place t
2615 (mapcar
2616 (lambda (m) (if (eql m 1) 12 (1- m)))
2617 months))))
2618 (+ n (/ (1- day) 7)))))
2619 (rrule (delq nil
2620 `((FREQ MONTHLY)
2621 ,(when months
2622 (list 'BYMONTH months))
2623 (BYDAY ((,dow . ,offset))))))
2624 (dtstart
2625 (calendar-nth-named-day n dow
2626 (if months (apply #'min months) 1)
2627 di:recurring-start-year
2628 (unless (eq day 'last) day))))
2629
2630 ;; if at this point we have an offset which could put us outside the
2631 ;; month boundaries, warn the user that this may not be supported:
2632 (when (< 4 (abs offset))
2633 (ical:warn
2634 (format
2635 "`diary-float' with large N=%d may not be supported on other systems" n)))
2636
2637 (list (ical:make-property ical:dtstart dtstart
2638 (ical:valuetypeparam 'ical:date))
2639 (ical:make-property ical:rrule rrule))))
2640
2641(defun di:offset-sexp-to-nodes (sexp)
2642 "Convert a `diary-offset' SEXP to a list of property nodes.
2643
2644SEXP must have the form (diary-offset INNER-SEXP NDAYS). The conversion
2645is only possible for relatively simple cases of INNER-SEXP. The
2646INNER-SEXP is first converted to a list of property nodes (see
2647`diary-icalendar-export-sexp'), and then any date, time, period, and
2648recurrence rule values in these nodes are adjusted NDAYS forward."
2649 (let* ((arg1 (nth 1 sexp))
2650 (inner-sexp (if (eq (car arg1) 'quote)
2651 (eval arg1 nil) ; unquote a quoted inner sexp
2652 arg1))
2653 (nodes (di:sexp-to-nodes inner-sexp))
2654 (ndays (nth 2 sexp)))
2655 (dolist (node nodes)
2656 (ical:with-property node nil
2657 (cl-case (ical:ast-node-type node)
2658 ((ical:dtstart ical:dtend)
2659 (ical:ast-node-set-value
2660 value-node
2661 (ical:date/time-add value :day ndays)))
2662 (ical:exdate
2663 (dolist (val-node value-nodes)
2664 (ical:with-node-value val-node nil
2665 (ical:ast-node-set-value
2666 val-node
2667 (ical:date/time-add value :day ndays)))))
2668 (ical:rdate
2669 (dolist (val-node value-nodes)
2670 (ical:ast-node-set-value
2671 val-node
2672 (ical:with-node-value val-node nil
2673 (cl-typecase value
2674 (ical:period
2675 (ical:make-period
2676 (ical:date/time-add (ical:period-start value) :day ndays)
2677 :end (when (ical:period--defined-end value)
2678 (ical:date/time-add
2679 (ical:period--defined-end value) :day ndays))
2680 :duration (ical:period-dur-value value)))
2681 (t (ical:date/time-add value :day ndays)))))))
2682 (ical:rrule
2683 (let ((mdays (ical:recur-by* 'BYMONTHDAY value))
2684 (ydays (ical:recur-by* 'BYYEARDAY value))
2685 (dows (ical:recur-by* 'BYDAY value))
2686 (bad-clause
2687 (cond ((ical:recur-by* 'BYSETPOS value) 'BYSETPOS)
2688 ((ical:recur-by* 'BYWEEKNO value) 'BYWEEKNO))))
2689 ;; We can't reliably subtract days in the following cases, so bail:
2690 (when (< 28 ndays)
2691 (di:signal-export-error
2692 (format "Cannot export `diary-offset' with large offset %d" ndays)))
2693 (when bad-clause
2694 (di:signal-export-error
2695 (format "Cannot export `diary-offset': inner SEXP %s contains %s"
2696 sexp bad-clause)))
2697 (when (seq-some (lambda (md)
2698 (or (and (< 0 md) (< 28 (+ md ndays)))
2699 (and (< md 0) (< 0 (+ md ndays)))))
2700 mdays)
2701 (di:signal-export-error
2702 (format "Cannot export `diary-offset': inner SEXP %s contains %s"
2703 inner-sexp
2704 "BYMONTHDAY clause that could cross month bounds")))
2705 (when (seq-some (lambda (yd)
2706 (or (and (< 0 yd) (< 365 (+ yd ndays)))
2707 (and (< yd 0) (< 0 (+ yd ndays)))))
2708 ydays)
2709 (di:signal-export-error
2710 (format "Cannot export `diary-offset': inner SEXP %s contains %s"
2711 inner-sexp
2712 "BYYEARDAY clause that could cross year bounds")))
2713 ;; Adjust the rule's clauses to account for the offset:
2714 (when mdays
2715 (setf (alist-get 'BYMONTHDAY value)
2716 (list
2717 (mapcar (apply-partially #'+ ndays) mdays))))
2718 (when ydays
2719 (setf (alist-get 'BYYEARDAY value)
2720 (list
2721 (mapcar (apply-partially #'+ ndays) ydays))))
2722 (when dows
2723 (setf (alist-get 'BYDAY value)
2724 (list
2725 (mapcar
2726 (lambda (dow)
2727 (if (integerp dow)
2728 (mod (+ dow ndays) 7)
2729 (let* ((wkday (car dow))
2730 (shifted (+ wkday ndays))
2731 (new-wkday (mod shifted 7))
2732 (new-offs
2733 (cond
2734 ;; if shifted is not between 0 and 7,
2735 ;; we moved into another week, so we need
2736 ;; to modify the offset within the month/year
2737 ;; by the number of weeks moved:
2738 ((< 7 shifted)
2739 (+ (/ shifted 7) (cdr dow)))
2740 ((< shifted 0)
2741 (+ -1 (/ shifted 7) (cdr dow)))
2742 ;; otherwise it stays the same:
2743 (t (cdr dow)))))
2744 (cons new-wkday new-offs))))
2745 dows)))))))))
2746 ;; Return the modified nodes:
2747 nodes))
2748
2749;; Converts a legacy value of `icalendar-export-alarms' to new format of
2750;; `diary-icalendar-export-alarms':
2751(defun di:-convert-legacy-alarm-options (alarm-options)
2752 (declare (obsolete nil "31.1"))
2753 (let ((lead-time (car alarm-options))
2754 (by-types (cadr alarm-options)))
2755 (mapcar
2756 (lambda (l)
2757 (cl-case (car l)
2758 (audio `(audio ,lead-time))
2759 (display `(display ,lead-time "%s"))
2760 (email `(email ,lead-time "%s" ,(cadr l)))))
2761 by-types)))
2762
2763(defun di:add-valarms (component &optional vtimezone)
2764 "Add VALARMs to COMPONENT according to `diary-icalendar-export-alarms'.
2765
2766COMPONENT should be an `icalendar-vevent' or `icalendar-vtodo'. The
2767generated VALARM components will be added to this node's children.
2768VTIMEZONE should define the local timezone; it is required when
2769formatting alarms as mail messages. Returns the modified COMPONENT."
2770 (let* ((alarm-options
2771 (if (and (bound-and-true-p icalendar-export-alarms)
2772 (null di:export-alarms))
2773 ;; For backward compatibility with icalendar.el:
2774 (with-suppressed-warnings
2775 ((obsolete ical:export-alarms
2776 di:-convert-legacy-alarm-options))
2777 (di:-convert-legacy-alarm-options ical:export-alarms))
2778 di:export-alarms))
2779 valarms)
2780 (dolist (opts alarm-options)
2781 (let* ((type (nth 0 opts))
2782 (minutes (nth 1 opts)))
2783 (cl-case type
2784 (audio
2785 (push (ical:make-valarm
2786 (ical:action "AUDIO")
2787 (ical:trigger (make-decoded-time :minute (* -1 minutes))))
2788 valarms))
2789 (display
2790 (ical:with-component component
2791 ((ical:summary :value summary)
2792 (ical:description :value description))
2793 (let* ((displayed-summary
2794 (replace-regexp-in-string
2795 "%t" (number-to-string minutes)
2796 (replace-regexp-in-string
2797 "%s" summary
2798 (nth 2 opts)))))
2799 (push (ical:make-valarm
2800 (ical:action "DISPLAY")
2801 (ical:trigger (make-decoded-time :minute (* -1 minutes)))
2802 (ical:summary displayed-summary)
2803 (ical:description description))
2804 valarms))))
2805 (email
2806 (ical:with-component component
2807 ((ical:summary :value summary)
2808 (ical:attendee :all entry-attendees))
2809 (let* ((subject
2810 (replace-regexp-in-string
2811 "%t" (number-to-string minutes)
2812 (replace-regexp-in-string
2813 "%s" summary
2814 (nth 2 opts))))
2815 (index (ical:index-insert-tz (ical:make-index) vtimezone))
2816 (body
2817 (dlet ((ical-as-alarm 'email))
2818 (di:format-entry component index)))
2819 (addresses (nth 3 opts))
2820 all-attendees)
2821 (dolist (address addresses)
2822 (cond
2823 ((eq address 'from-entry)
2824 (setq all-attendees (append entry-attendees all-attendees)))
2825 ((stringp address)
2826 (push (ical:make-property ical:attendee
2827 (concat "mailto:" address))
2828 all-attendees))))
2829 (push (ical:make-valarm
2830 (ical:action "EMAIL")
2831 (ical:trigger (make-decoded-time :minute (* -1 minutes)))
2832 (ical:summary subject)
2833 (ical:description body)
2834 (@ all-attendees))
2835 valarms)))))))
2836 (apply #'ical:ast-node-adopt-children component valarms)
2837 component))
2838
2839(defun di:rrule-sexp-to-recurrence (sexp &optional vtimezone)
2840 "Convert a `diary-rrule' SEXP to iCalendar recurrence rule properties.
2841Returns a list containing at least `icalendar-dtstart' and
2842`icalendar-rrule' nodes, and zero or more `icalendar-rdate',
2843`icalendar-exdate', and `icalendar-duration' nodes.
2844
2845VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in
2846SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating
2847times according to `diary-icalendar-time-zone-export-strategy'."
2848 (let* ((args (cdr sexp))
2849 (start (plist-get args :start))
2850 (dtstart (di:convert-time-via-strategy
2851 (if (eq 'quote (car start)) (eval start nil) start)
2852 vtimezone))
2853 (rule (plist-get args :rule))
2854 (rrule (if (eq 'quote (car rule)) (eval rule nil) rule))
2855 (included (plist-get args :include))
2856 (rdates (mapcar
2857 (lambda (dt) (di:convert-time-via-strategy dt vtimezone))
2858 (if (eq 'quote (car included)) (eval included nil) included)))
2859 (excluded (plist-get args :exclude))
2860 (exdates (mapcar
2861 (lambda (dt) (di:convert-time-via-strategy dt vtimezone))
2862 (if (eq 'quote (car excluded)) (eval excluded nil) excluded)))
2863 (duration (eval (plist-get args :duration) t))
2864 (dur-value
2865 (if (eq 'quote (car duration)) (eval duration nil) duration))
2866 (tzid
2867 (when (cl-typep dtstart 'ical:date-time)
2868 (ical:with-property-of vtimezone 'ical:tzid)))
2869 nodes)
2870 (push (ical:make-property ical:rrule rrule) nodes)
2871 (push (ical:make-property ical:dtstart dtstart (ical:tzidparam tzid))
2872 nodes)
2873 (when rdates
2874 (push (ical:make-property ical:rdate rdates (ical:tzidparam tzid))
2875 nodes))
2876 (when exdates
2877 (push (ical:make-property ical:exdate exdates (ical:tzidparam tzid))
2878 nodes))
2879 (when duration
2880 (push (ical:make-property ical:duration dur-value) nodes))
2881 nodes))
2882
2883(defun di:dates-to-recurrence (months days years)
2884 "Convert values representing one or more dates to iCalendar recurrences.
2885
2886MONTHS, DAYS, and YEARS should either be integers, lists of integers, or
2887the symbol t.
2888
2889Returns a pair of nodes (START R), where START is an `icalendar-dtstart'
2890node and R is an `icalendar-rrule' node or `icalendar-rdate' node (or
2891nil, if MONTHS, DAYS and YEARS are all integers)."
2892 (if (and (integerp months) (integerp days) (integerp years))
2893 ;; just a regular date, without recurrence data:
2894 (list
2895 (ical:make-property ical:dtstart (list months days years))
2896 nil)
2897
2898 (when (integerp months) (setq months (list months)))
2899 (when (integerp days) (setq days (list days)))
2900 (when (integerp years) (setq years (list years)))
2901 (let (dtstart freq bymonth bymonthday rdates rdate-type)
2902 (cond ((and (eq days t) (eq months t) (eq years t))
2903 (setq freq 'DAILY
2904 dtstart (list 1 1 di:recurring-start-year)))
2905 ((and (eq months t) (eq years t))
2906 (setq freq 'MONTHLY
2907 bymonthday days
2908 dtstart (list 1 (car days) di:recurring-start-year)))
2909 ((and (eq years t) (eq days t))
2910 (setq freq 'DAILY
2911 bymonth months
2912 dtstart (list (apply #'min months)
2913 1
2914 di:recurring-start-year)))
2915 ((eq years t)
2916 (setq freq 'YEARLY
2917 bymonth months
2918 bymonthday days
2919 dtstart
2920 (list (apply #'min months)
2921 (apply #'min days)
2922 di:recurring-start-year)))
2923 ;; The remaining cases are not representable as RRULEs,
2924 ;; because there is no BYYEAR clause. So we generate an RDATE
2925 ;; covering each specified date.
2926 ((and (eq months t) (eq days t))
2927 ;; In this case we represent each of the specified years as a period:
2928 (setq rdate-type 'ical:period
2929 rdates
2930 (mapcar
2931 (lambda (y)
2932 (ical:make-period
2933 (ical:make-date-time :year y :month 1 :day 1
2934 :hour 0 :minute 0 :second 0)
2935 :end
2936 (ical:make-date-time :year (1+ y) :month 1 :day 1
2937 :hour 0 :minute 0 :second 0)))
2938 years)
2939 dtstart (ical:date-time-to-date
2940 (ical:period-start (car rdates)))))
2941 (t
2942 ;; Otherwise, represent each date individually:
2943 (setq rdate-type 'ical:date
2944 rdates
2945 (mapcan
2946 (lambda (y)
2947 (mapcan
2948 (lambda (m)
2949 (mapcar
2950 (lambda (d) (list m d y))
2951 (if (listp days) days
2952 ;; days = t:
2953 (number-sequence 1 (calendar-last-day-of-month m y)))))
2954 (if (listp months) months
2955 ;; months = t:
2956 (number-sequence 1 12))))
2957 years)
2958 ;; ensure dtstart is the earliest recurrence:
2959 dtstart (apply #'ical:date/time-min rdates)
2960 rdates (seq-remove (apply-partially #'equal dtstart) rdates))))
2961
2962 ;; Return the pair of nodes (DTSTART RRULE) or (DTSTART RDATE):
2963 (let* ((recur-value
2964 (delq nil
2965 `((FREQ ,freq)
2966 ,(when bymonth (list 'BYMONTH bymonth))
2967 ,(when bymonthday (list 'BYMONTHDAY bymonthday)))))
2968 (rrule-node (when freq (ical:make-property ical:rrule recur-value)))
2969 (rdate-node (when rdates
2970 (ical:make-property ical:rdate rdates
2971 (ical:valuetypeparam rdate-type))))
2972 (dtstart-node (ical:make-property ical:dtstart dtstart)))
2973 (list dtstart-node (or rrule-node rdate-node))))))
2974
2975(defun di:date-sexp-to-recurrence (sexp)
2976 "Convert a `diary-date' SEXP to an `icalendar-rrule' or `icalendar-rdate' node.
2977Returns a pair of nodes (START R), where START is an `icalendar-dtstart'
2978node and R is the RRULE or RDATE node."
2979 (let* ((d1 (nth 1 sexp))
2980 (d2 (nth 2 sexp))
2981 (d3 (nth 3 sexp))
2982 years months days)
2983 (cl-case calendar-date-style
2984 (iso (setq years (if (integerp d1) (list d1) d1)
2985 months (if (integerp d2) (list d2) d2)
2986 days (if (integerp d3) (list d3) d3)))
2987 (american (setq months (if (integerp d1) (list d1) d1)
2988 days (if (integerp d2) (list d2) d2)
2989 years (if (integerp d3) (list d3) d3)))
2990 (european (setq days (if (integerp d1) (list d1) d1)
2991 months (if (integerp d2) (list d2) d2)
2992 years (if (integerp d3) (list d3) d3))))
2993
2994 ;; unquote lists of integers read as quoted lists:
2995 (when (and (listp months) (eq 'quote (car months)))
2996 (setq months (eval months nil)))
2997 (when (and (listp days) (eq 'quote (car days)))
2998 (setq days (eval days nil)))
2999 (when (and (listp years) (eq 'quote (car years)))
3000 (setq years (eval years nil)))
3001
3002 ;; if at this point we don't have lists of integers or "t", user
3003 ;; entered a malformed diary-date sexp:
3004 (unless (or (eq months t) (seq-every-p #'integerp months))
3005 (di:signal-export-error
3006 (format "Malformed months in `diary-date' S-expression:\n%s" sexp)))
3007 (unless (or (eq days t) (seq-every-p #'integerp days))
3008 (di:signal-export-error
3009 (format "Malformed days in `diary-date' S-expression:\n%s" sexp)))
3010 (unless (or (eq years t) (seq-every-p #'integerp years))
3011 (di:signal-export-error
3012 (format "Malformed years in `diary-date' S-expression:\n%s" sexp)))
3013
3014 (di:dates-to-recurrence months days years)))
3015
3016(defun di:other-sexp-to-recurrence (sexp)
3017 "Convert diary SEXP to `icalendar-rdate' by enumerating its recurrences.
3018
3019The enumeration starts on the current date and includes recurrences in
3020the next `diary-icalendar-export-sexp-enumeration-days' days. Returns a
3021list (START COMMENT RDATE), where START is an `icalendar-dtstart',
3022COMMENT is an `icalendar-comment' containing SEXP, and RDATE is an
3023`icalendar-rdate' containing the enumerated recurrences. If there are
3024no recurrences, (START COMMENT EXDATE) is returned, where START is the
3025current date, and EXDATE is an `icalendar-exdate' excluding that start
3026date as a recurrence. (This is because `icalendar-dtstart' is a required
3027property and must be present even if the recurrence set is empty.)"
3028 (let* ((today (calendar-absolute-from-gregorian (calendar-current-date)))
3029 (end (+ today (1- di:export-sexp-enumeration-days)))
3030 dtstart rdates exdates)
3031 (dolist (absdate (number-sequence today end))
3032 (calendar-dlet ((date (calendar-gregorian-from-absolute absdate)))
3033 (when (eval sexp t)
3034 (push date rdates))))
3035 (if rdates
3036 (progn
3037 (setq rdates (nreverse rdates))
3038 (setq dtstart (car rdates)
3039 rdates (cdr rdates)))
3040 (ical:warn
3041 (format "No recurrences in the next %d days: %s"
3042 di:export-sexp-enumeration-days
3043 sexp)
3044 :severity 0)
3045 ;; When there are no recurrences, we still need a DTSTART, but we
3046 ;; can exclude it via an EXDATE:
3047 (setq dtstart (calendar-current-date)
3048 exdates (list dtstart)))
3049
3050 (append
3051 (list
3052 (ical:make-property ical:dtstart dtstart
3053 (ical:valuetypeparam 'ical:date))
3054 ;; TODO: should we maybe use an X-name property for this?
3055 (ical:make-property ical:comment (format "%s" sexp)))
3056 (if rdates
3057 (list
3058 (ical:make-property ical:rdate rdates
3059 (ical:valuetypeparam 'ical:date)))
3060 (list
3061 (ical:make-property ical:exdate exdates
3062 (ical:valuetypeparam 'ical:date)))))))
3063
3064(defun di:sexp-to-nodes (sexp &optional vtimezone)
3065 "Convert a diary S-expression SEXP to a list of iCalendar property nodes.
3066
3067The fully supported S-expressions are:
3068`diary-anniversary'
3069`diary-block'
3070`diary-cyclic'
3071`diary-date'
3072`diary-float'
3073`diary-remind'
3074`diary-rrule'
3075`diary-time-block'
3076
3077There is partial support for `diary-offset' S-expressions; see
3078`diary-icalendar-offset-to-nodes'.
3079
3080Other S-expressions are only supported via enumeration. Their
3081recurrences are enumerated for
3082`diary-icalendar-export-sexp-enumeration-days' starting from the current
3083date; see `diary-icalendar-other-sexp-to-recurrence'. If
3084`diary-icalendar-export-sexp-enumerate-all' is non-nil, all
3085S-expressions are enumerated rather than converted to recurrence rules.
3086
3087VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in
3088SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating
3089times according to `diary-icalendar-time-zone-export-strategy'."
3090 (if di:export-sexp-enumerate-all ;; see Bug#7911 for motivation
3091 (di:other-sexp-to-recurrence sexp)
3092 (cl-case (car sexp)
3093 (diary-anniversary (di:anniversary-sexp-to-recurrence sexp))
3094 (diary-block (di:block-sexp-to-recurrence sexp))
3095 (diary-cyclic (di:cyclic-sexp-to-recurrence sexp))
3096 (diary-date (di:date-sexp-to-recurrence sexp))
3097 (diary-float (di:float-sexp-to-recurrence sexp))
3098 (diary-offset (di:offset-sexp-to-nodes sexp))
3099 (diary-rrule (di:rrule-sexp-to-recurrence sexp vtimezone))
3100 (diary-time-block (di:time-block-sexp-to-start-end sexp vtimezone))
3101 ;; For `diary-remind' we only handle the inner sexp:
3102 (diary-remind (di:sexp-to-nodes (nth 1 sexp) vtimezone))
3103 (t (di:other-sexp-to-recurrence sexp)))))
3104
3105;;; Time zone handling during export:
3106
3107(defun di:current-tz-to-vtimezone (&optional tz tzid start-year)
3108 "Convert TZ to an `icalendar-vtimezone'.
3109
3110See `icalendar-recur-current-tz-to-vtimezone' for arguments' meanings.
3111This function wraps that one, but signals `icalendar-diary-export-error'
3112instead if TZ cannot be converted."
3113 (condition-case _
3114 (icr:current-tz-to-vtimezone tz tzid start-year)
3115 ((ical:tz-insufficient-data ical:tz-unsupported)
3116 (di:signal-export-error
3117 (format "Unable to export time zone data: %s.\n%s." tz
3118 "Check the value of `diary-icalendar-time-zone-export-strategy'")))))
3119
3120;;; Parsing complete diary entries:
3121
3122(defun di:parse-entry-linewise (begin end vtimezone type date-nodes)
3123 "Convert the entry between BEGIN and END linewise to iCalendar components.
3124
3125\"Linewise\" means each line of a diary entry will be exported as a
3126distinct event; see `diary-icalendar-export-linewise'.
3127Returns a list of component nodes representing the events.
3128
3129VTIMEZONE must be the `icalendar-vtimezone' in which times in the entry
3130appear (or nil). TYPE and DATE-NODES must contain the iCalendar component
3131type and date information parsed from the beginning of the entry which
3132apply to all of the events. These arguments are passed on in recursive
3133calls to `diary-icalendar-parse-entry'."
3134 (save-restriction
3135 (narrow-to-region begin end)
3136 (goto-char (point-min))
3137 (let ((subentry-regexp
3138 ;; match to the end of lines which have indentation equal to
3139 ;; or greater than the current one:
3140 (rx line-start
3141 (group-n 1 (+ space))
3142 (* not-newline)
3143 (* "\n" (backref 1) (+ space) (* not-newline))))
3144 components)
3145
3146 (while (re-search-forward subentry-regexp end t)
3147 (let ((next-pos (1+ (match-end 0))))
3148 (setq components
3149 (append
3150 (di:parse-entry (match-beginning 0) (match-end 0)
3151 vtimezone type date-nodes)
3152 components))
3153 (goto-char next-pos)))
3154 components)))
3155
3156(defun di:parse-entry (begin end &optional vtimezone type date-nodes)
3157 "Convert the entry between BEGIN and END to a list of iCalendar components.
3158
3159The region between BEGIN and END will be parsed for a date, time,
3160summary, description, attendees, and UID. This information will be
3161combined into an `icalendar-vevent' (or `icalendar-vjournal' or
3162`icalendar-vtodo', depending on the values of
3163`diary-icalendar-export-nonmarking-entries',
3164`diary-icalendar-export-nonmarking-as-vjournal' and
3165`diary-icalendar-todo-regexp') and that component will be returned
3166wrapped in a list. Returns nil if the entry should not be exported
3167according to `diary-icalendar-export-nonmarking-entries'.
3168
3169If `diary-icalendar-export-linewise' is non-nil, then a top-level call
3170to this function will return a list of several such components. (Thus,
3171the function always returns a list of components.)
3172
3173VTIMEZONE, if specified, should be the `icalendar-vtimezone' in which
3174times in the entry appear. If
3175`diary-icalendar-time-zone-export-strategy' is not either \\='to-utc or
3176\\='floating, VTIMEZONE must be provided.
3177
3178DATE-NODES and TYPE should be nil in a top-level call; they are used in
3179recursive calls to this function made by
3180`diary-icalendar-parse-entry-linewise'."
3181 (save-restriction
3182 (narrow-to-region begin end)
3183 (goto-char (point-min))
3184 (let (sexp dateform weekday tzid transparency all-props should-recurse)
3185 (setq should-recurse (and di:export-linewise (not date-nodes) (not type)))
3186 (when (ical:vtimezone-component-p vtimezone)
3187 (setq tzid (ical:with-property-of vtimezone 'ical:tzid)))
3188 (unless date-nodes
3189 ;; If we don't already have date information, we are in a
3190 ;; top-level call and need to collect the date and type
3191 ;; information from the start of the entry:
3192 (setq type (di:parse-entry-type))
3193 ;; N.B. the following four parsing functions successively
3194 ;; narrow the current restriction past anything they parse:
3195 (setq transparency (di:parse-transparency type))
3196 (setq sexp (di:parse-sexp))
3197 (setq dateform (di:parse-date-form))
3198 (setq weekday (di:parse-weekday-name))
3199 (setq date-nodes
3200 (append
3201 transparency
3202 (when sexp (di:sexp-to-nodes sexp vtimezone))
3203 (when dateform
3204 (apply #'di:dates-to-recurrence dateform))
3205 (when (and weekday (not dateform))
3206 (di:weekday-to-recurrence weekday)))))
3207
3208 (when type ; nil means entry should not be exported
3209 (if should-recurse
3210 ;; If we are in a top level call and should export linewise,
3211 ;; do that recursively now:
3212 (di:parse-entry-linewise (point) end vtimezone type date-nodes)
3213
3214 ;; Otherwise, we are either in a recursive call with a
3215 ;; narrower restriction, or don't need to export linewise. In
3216 ;; both cases, we gather the remaining data from the current
3217 ;; restriction and combine everything into a component node:
3218 (let* ((times (di:parse-time))
3219 (start-time (when times (car times)))
3220 (end-time (when times (cadr times))))
3221 ;; Combine clock time values in the current restriction with
3222 ;; date information parsed at the top level. Doing this here
3223 ;; allows us to combine a different time on each line of an
3224 ;; entry exported linewise with the date information for the
3225 ;; whole entry:
3226 (dolist (node date-nodes)
3227 (ical:with-property node nil
3228 (cond
3229 ((and (ical:dtstart-property-p node)
3230 (eq 'ical:date value-type)
3231 start-time)
3232 (let ((dtstart
3233 (di:convert-time-via-strategy
3234 (ical:date-time-variant
3235 start-time
3236 :year (calendar-extract-year value)
3237 :month (calendar-extract-month value)
3238 :day (calendar-extract-day value))
3239 vtimezone)))
3240 (push (ical:make-property ical:dtstart dtstart
3241 (ical:tzidparam tzid))
3242 all-props)
3243 (when end-time
3244 ;; an end time parsed from a time specification
3245 ;; in the entry is always on the same day as
3246 ;; DTSTART.
3247 (let* ((dtend
3248 (di:convert-time-via-strategy
3249 (ical:date-time-variant
3250 end-time
3251 :year (calendar-extract-year value)
3252 :month (calendar-extract-month value)
3253 :day (calendar-extract-day value))
3254 vtimezone))
3255 (is-recurring
3256 (seq-find
3257 (lambda (n) (or (ical:rrule-property-p n)
3258 (ical:rdate-property-p n)))
3259 date-nodes)))
3260 (if is-recurring
3261 ;; If the entry is recurring, we interpret
3262 ;; the end time as giving us a duration for all
3263 ;; recurrences:
3264 (progn
3265 (when (seq-find #'ical:duration-property-p
3266 date-nodes)
3267 (ical:warn
3268 (concat "Parsed both duration and end time; "
3269 "ignoring end time specification")
3270 :buffer (current-buffer)
3271 :position (point)))
3272 (push (ical:make-property ical:duration
3273 (ical:duration-between dtstart dtend))
3274 all-props))
3275 ;; Otherwise we make a normal DTEND:
3276 (push (ical:make-property ical:dtend dtend)
3277 all-props))))))
3278
3279 ((and (ical:rdate-property-p node)
3280 start-time
3281 (seq-every-p (apply-partially #'eq 'ical:date)
3282 value-types))
3283 (let ((rdates
3284 (mapcar
3285 (lambda (dt)
3286 (if end-time
3287 (ical:make-period
3288 (di:convert-time-via-strategy
3289 (ical:date-time-variant
3290 start-time
3291 :year (calendar-extract-year dt)
3292 :month (calendar-extract-month dt)
3293 :day (calendar-extract-day dt))
3294 vtimezone)
3295 :end
3296 (di:convert-time-via-strategy
3297 (ical:date-time-variant
3298 end-time
3299 :year (calendar-extract-year dt)
3300 :month (calendar-extract-month dt)
3301 :day (calendar-extract-day dt))
3302 vtimezone))
3303 (di:convert-time-via-strategy
3304 (ical:date-time-variant
3305 start-time
3306 :year (calendar-extract-year dt)
3307 :month (calendar-extract-month dt)
3308 :day (calendar-extract-day dt))
3309 vtimezone)))
3310 values)))
3311 (push (ical:make-property ical:rdate rdates
3312 (ical:tzidparam tzid))
3313 all-props)))
3314
3315 ;; preserve any other node read from date, e.g. RRULE, as is:
3316 (node (push node all-props))))))
3317
3318 ;; In a VTODO, entry date must become the DUE date; either
3319 ;; DTEND becomes DUE, or if there is no DTEND, then DTSTART:
3320 (when (eq type 'ical:vtodo)
3321 (unless (catch 'found-dtend
3322 (dolist (node all-props)
3323 (when (ical:dtend-property-p node)
3324 (ical:ast-node-set-type node 'ical:due)
3325 (throw 'found-dtend t))))
3326 (dolist (node all-props)
3327 (when (ical:dtstart-property-p node)
3328 (ical:ast-node-set-type node 'ical:due)))))
3329
3330 ;; Collect the remaining properties:
3331 (setq all-props (append (di:parse-summary-and-description) all-props))
3332 (setq all-props (append (di:parse-attendees-and-organizer) all-props))
3333 (push (ical:make-property ical:dtstamp (decode-time nil t)) all-props)
3334 (let ((class (di:parse-class))
3335 (location (di:parse-location))
3336 (status (di:parse-status))
3337 (url (di:parse-url)))
3338 (when class (push class all-props))
3339 (when location (push location all-props))
3340 (when status (push status all-props))
3341 (when url (push url all-props)))
3342 (push (or (di:parse-uid)
3343 (ical:make-property ical:uid
3344 (ical:make-uid all-props)))
3345 all-props)
3346
3347 ;; Allow users to add to the properties parsed:
3348 (when (functionp di:other-properties-parser)
3349 (let ((others (funcall di:other-properties-parser type all-props)))
3350 (dolist (p others)
3351 (condition-case nil
3352 (push (ical:ast-node-valid-p p)
3353 all-props)
3354 (ical:validation-error
3355 (ical:warn
3356 (format "`%s' returned invalid `%s' property; ignoring"
3357 di:other-properties-parser
3358 (ical:ast-node-type p))
3359 :buffer (current-buffer)
3360 :position (point)))))))
3361
3362 ;; Construct, validate and return a component of the appropriate type:
3363 (let ((component
3364 (ical:ast-node-valid-p
3365 (ical:make-ast-node type nil all-props))))
3366
3367 ;; Add alarms per `diary-icalendar-export-alarms', except for
3368 ;; in VJOURNAL, where alarms are not allowed:
3369 ;; TODO: should we also add alarms for `diary-remind' sexps?
3370 (when (not (eq type 'ical:vjournal))
3371 (di:add-valarms component vtimezone))
3372
3373 ;; Return the component wrapped in a list (for type consistency):
3374 (list component)))))))
3375
3376;;;###autoload
3377(defun di:export-region (begin end filename &optional erase)
3378 "Export diary entries between BEGIN and END to iCalendar format in FILENAME.
3379
3380If FILENAME exists and is not empty, this function asks whether to erase
3381its contents first. If ERASE is non-nil, the contents of FILENAME will
3382always be erased without asking. Otherwise the exported data will be
3383appended to the end of FILENAME.
3384
3385The export depends on a number of user-customizable variables. Before
3386running this command for the first time, you may especially wish to
3387check the values of:
3388`diary-file'
3389`diary-date-forms'
3390`calendar-date-style'
3391as well as variables in the customization group `diary-icalendar-export'."
3392 (interactive (list (region-beginning)
3393 (region-end)
3394 (expand-file-name
3395 (read-file-name "iCalendar file: "))))
3396
3397 (ical:init-error-buffer)
3398 (let (output-buffer local-tz components vcalendar)
3399 (when (and (null erase)
3400 (file-exists-p filename)
3401 (< 0 (file-attribute-size (file-attributes filename)))
3402 (y-or-n-p (format "Delete existing contents of %s?" filename)))
3403 (setq erase t))
3404 (ical:condition-case err
3405 (setq output-buffer (find-file-noselect filename)))
3406 (when output-buffer
3407 (save-excursion
3408 (save-restriction
3409 (narrow-to-region begin end)
3410 (goto-char (point-min))
3411 (cond ((eq 'local di:time-zone-export-strategy)
3412 (setq local-tz (di:current-tz-to-vtimezone)))
3413 ((listp di:time-zone-export-strategy)
3414 (setq local-tz (di:current-tz-to-vtimezone
3415 di:time-zone-export-strategy))))
3416 (while (re-search-forward di:entry-regexp nil t)
3417 (let ((entry-start (match-beginning 0))
3418 (entry-end (match-end 0))
3419 (first-line (match-string 1)))
3420 (ical:condition-case err-data
3421 (setq components
3422 (append (di:parse-entry entry-start entry-end local-tz)
3423 components))
3424 (ical:export-error
3425 (ical:warn
3426 (concat
3427 (format "Unable to export entry \"%s...\"; skipping" first-line)
3428 "\nError was:\n"
3429 (plist-get err-data :message))
3430 :position entry-start
3431 :buffer (current-buffer))))
3432 (goto-char (1+ entry-end))))
3433 (setq components (nreverse components))
3434 (when local-tz (push local-tz components))
3435 (ical:condition-case err-data
3436 (setq vcalendar (ical:make-vcalendar (@ components))))
3437
3438 (when vcalendar
3439 (with-current-buffer output-buffer
3440 (when erase (erase-buffer))
3441 (goto-char (point-max)) ; append, if user chose not to erase
3442 (unless (bolp) (insert "\n"))
3443 (ical:condition-case err-data
3444 (insert (ical:print-calendar-node vcalendar)))
3445 (let ((coding-system-for-write 'utf-8-dos))
3446 (save-buffer))))))))
3447
3448 (message
3449 (if (ical:errors-p)
3450 (format "iCalendar export completed with errors; see buffer %s"
3451 (buffer-name (ical:error-buffer)))
3452 "iCalendar export completed successfully.")))
3453
3454;;;###autoload
3455(defun di:export-file (diary-filename filename &optional erase)
3456 "Export DIARY-FILENAME to iCalendar format in FILENAME.
3457
3458The diary entries in DIARY-FILENAME will be exported to iCalendar format
3459and the resulting calendar will be saved to FILENAME.
3460
3461If FILENAME exists and is not empty, this function asks whether to erase
3462its contents first. If ERASE is non-nil, the contents of FILENAME will
3463always be erased without asking. Otherwise the exported data will be
3464appended to the end of FILENAME.
3465
3466The export depends on a number of user-customizable variables. Before
3467running this command for the first time, you may especially wish to
3468check the values of:
3469`diary-file'
3470`diary-date-forms'
3471`calendar-date-style'
3472as well as variables in the customization group `diary-icalendar-export'."
3473 (interactive (list
3474 (read-file-name "Diary file: "
3475 (when diary-file (file-name-directory diary-file))
3476 (cons diary-file diary-included-files)
3477 'confirm)
3478 (read-file-name "iCalendar file: "
3479 (when diary-file (file-name-directory diary-file))
3480 (when diary-file
3481 (concat
3482 (file-name-sans-extension diary-file)
3483 ".ics")))))
3484 (when (and (null erase)
3485 (file-exists-p filename)
3486 (< 0 (file-attribute-size (file-attributes filename)))
3487 (y-or-n-p (format "Delete existing contents of %s?" filename)))
3488 (setq erase t))
3489 (with-current-buffer (find-file-noselect diary-filename)
3490 (di:export-region (point-min) (point-max) filename erase)))
3491
3492
3493;; Display in Diary
3494
3495;;; Functions implementing diary-icalendar sexps.
3496;;; TODO: move these to diary-lib.el?
3497
3498;; To be called from diary-sexp-entry, where DATE, ENTRY are bound.
3499(cl-defun diary-time-block (&key start end)
3500 "Diary S-expression for time blocks.
3501
3502Entry applies if the queried date occurs between START and END,
3503inclusive. START and END may be `icalendar-date' or
3504`icalendar-date-time' values."
3505 (with-no-warnings (defvar date) (defvar entry))
3506 (when (and (ical:date/time<= start date) (ical:date/time<= date end))
3507 entry))
3508
3509;; To be called from diary-sexp-entry, where DATE, ENTRY are bound.
3510(cl-defun diary-rrule (&key rule start duration include exclude)
3511 "Diary S-expression for iCalendar recurrence rules.
3512
3513Entry applies if the queried date matches the recurrence rule.
3514
3515The keyword arguments RULE, START, INCLUDE and EXCLUDE should contain
3516the recurrence data from an iCalendar component. RULE should be an
3517`icalendar-recur' value, START an `icalendar-date' or
3518`icalendar-date-time', DURATION an `icalendar-dur-value', and INCLUDE
3519and EXCLUDE should be lists of `icalendar-date' or `icalendar-date-time'
3520values (of the same type as START)."
3521 ;; TODO: also support a format that is nicer to read and type by hand.
3522 ;; e.g. just letting a rule be specified in a recur-value string like
3523 ;; :rule "FREQ=MONTHLY;BYDAY=1SU"
3524 ;; is perhaps already better than the raw Lisp format. We could at least
3525 ;; support specifying the clauses with keywords, e.g.
3526 ;; :freq :monthly :byday '("Sunday" . 1)
3527 ;; would be better than the current
3528 ;; :rule '((FREQ MONTHLY) (BYDAY ((0 . 1))))
3529 (with-no-warnings (defvar date) (defvar entry))
3530 (when (ical:date<= start date)
3531 (let* ((vevent (ical:make-vevent
3532 (ical:rrule rule)
3533 (ical:dtstart start)
3534 (ical:rdate include)
3535 (ical:exdate exclude)))
3536 (interval (icr:find-interval date start rule)))
3537 (cl-typecase start
3538 (ical:date
3539 (when (member date (icr:recurrences-in-interval interval vevent))
3540 entry))
3541 (ical:date-time
3542 ;; TODO. If start is a date-time, it was probably imported from
3543 ;; an iCalendar file, but in order to calculate recurrences, we
3544 ;; really need all the time zone information from that file,
3545 ;; not just the rule, start, include and exclude. But encoding
3546 ;; all that tz info in a diary s-exp is cumbersome and ugly and
3547 ;; probably not worth the trouble. Since this is the diary, we
3548 ;; assume that all we really care about here is whether there
3549 ;; are recurrences on a particular day. Thus we convert
3550 ;; HOURLY/MINUTELY/SECONDLY rules to a DAILY rule, and all
3551 ;; values to plain dates. This keeps things simple (and
3552 ;; hopefully quicker) but means that information gets lost. I
3553 ;; hope this can be changed to do things right at some point,
3554 ;; but that will require first adding more robust time zone
3555 ;; support to the diary somehow -- perhaps via #included
3556 ;; iCalendar files?
3557 (let* ((date-rule (copy-sequence rule))
3558 (start-date (ical:date-time-to-date start))
3559 (include-dates (mapcar #'ical:date-time-to-date include))
3560 (exclude-dates (mapcar #'ical:date-time-to-date exclude))
3561 ;; Preserve the clock times in the entry:
3562 (entry-time
3563 (if duration
3564 (di:format-time-range
3565 start
3566 (ical:date/time-add-duration start duration))
3567 (di:format-time-as-local start)))
3568 (date-entry (concat entry-time " " entry)))
3569 (when (memq (ical:recur-freq date-rule) '(HOURLY MINUTELY SECONDLY))
3570 (setf (alist-get 'FREQ date-rule) 'DAILY)
3571 (setf (alist-get 'INTERVAL date-rule) 1)
3572 (setf (alist-get 'BYHOUR date-rule nil t) nil)
3573 (setf (alist-get 'BYMINUTE date-rule nil t) nil)
3574 (setf (alist-get 'BYSECOND date-rule nil t) nil))
3575 ;; Recurse with the plain date values:
3576 (calendar-dlet
3577 ((date date)
3578 (entry date-entry))
3579 (diary-rrule :rule date-rule :start start-date
3580 :include include-dates :exclude exclude-dates))))))))
3581
3582(defun di:display-entries ()
3583 "Display iCalendar data from a file in the diary.
3584
3585This function allows you to display the data in an iCalendar-formatted
3586file in the diary without importing it. The data is read directly from
3587the currently value of `diary-file'. If this file contains iCalendar
3588data, any events, tasks, and journal entries in the file which occur on
3589`original-date' and `number' of days after are formatted for display in
3590the diary. (All three of these variables are dynamically bound by the
3591diary when this function is called.)
3592
3593To use this function, add an '#include \"FILE\"' entry in your diary
3594file for each iCalendar file you want to display (see
3595`diary-include-string'). Then add `diary-include-other-diary-files' to
3596`diary-list-entries-hook'. (Consider also adding `diary-sort-entries' at
3597the end of this hook if you want entries to be displayed in order.)
3598Finally, add this function to `diary-nongregorian-listing-hook', so that
3599it is called once for each included file when the diary is displayed."
3600 (with-no-warnings (defvar original-date) ; the start date
3601 (defvar number) ; number of days to generate entries for
3602 (defvar diary-file)) ; dyn. bound to included file name
3603 (let ((diary-buffer (or (find-buffer-visiting diary-file)
3604 (find-file-noselect diary-file))))
3605 (when (ical:contains-vcalendar-p diary-buffer)
3606 (let ((vcal/idx (ical:parse-and-index diary-file)))
3607 (when vcal/idx
3608 (let* ((index (cadr vcal/idx))
3609 (absstart (calendar-absolute-from-gregorian original-date))
3610 (absend (+ absstart (1- number))))
3611
3612 (dolist (absdate (number-sequence absstart absend))
3613 (let* ((date (calendar-gregorian-from-absolute absdate))
3614 (to-format (ical:index-get index :date date)))
3615 (dolist (component to-format)
3616 ;; Format the entry, with a pointer back to its location
3617 ;; in the parsed buffer:
3618 (let ((marker (make-marker)))
3619 (set-marker marker
3620 (ical:ast-node-meta-get :begin component)
3621 (ical:ast-node-meta-get :buffer component))
3622 (diary-add-to-list
3623 date
3624 (di:format-entry component index)
3625 ""
3626 marker)))))))))))
3627
3628(defun di:marking-dates-of (component index)
3629 "Return the dates in COMPONENT that should be marked in the calendar.
3630
3631INDEX should be a parse tree index containing the time zone definition
3632relevant to COMPONENT; see `icalendar-parse-and-index'. The dates to
3633mark are derived from COMPONENT's start and end date and time, and any
3634recurrences it has within the year currently displayed by the calendar.
3635
3636No dates are returned if COMPONENT's `icalendar-transp' property has the
3637value \"TRANSPARENT\" (which means the component does not form a block
3638of busy time on a schedule), or if COMPONENT is an `icalendar-vjournal'
3639and `diary-icalendar-import-vjournal-as-nonmarking' is non-nil."
3640 (ical:with-component component
3641 ((ical:dtstart :first dtstart-node :value dtstart)
3642 (ical:dtend :first dtend-node :value dtend)
3643 (ical:due :value due)
3644 (ical:duration :value duration)
3645 (ical:rdate :first rdate)
3646 (ical:rrule :first rrule)
3647 (ical:transp :value transparency))
3648 (let* ((start-tz (ical:with-param-of dtstart-node 'ical:tzidparam
3649 (ical:index-get index :tzid value)))
3650 (end
3651 (cond
3652 (dtend dtend)
3653 (due due)
3654 (duration (ical:date/time-add-duration dtstart duration start-tz))))
3655 dates)
3656
3657 (unless (or (equal transparency "TRANSPARENT")
3658 (and di:import-vjournal-as-nonmarking
3659 (ical:vjournal-component-p component)))
3660 ;; Mark the start date(s) for every (marking) entry:
3661 (setq dates (if end
3662 (ical:dates-until dtstart end t)
3663 (list (ical:date/time-to-date
3664 (ical:date/time-to-local dtstart)))))
3665 ;; Mark the dates for any recurrences in the displayed calendar year:
3666 (let ((year (when (boundp 'displayed-year) ; bound by calendar
3667 displayed-year)))
3668 (when (and year (or rdate rrule))
3669 (let* ((low (list 1 1 year))
3670 (high (list 12 31 year))
3671 (recs (icr:recurrences-in-window-w/end-times
3672 low high component start-tz)))
3673 (dolist (rec recs)
3674 (setq dates (append (ical:dates-until (car rec) (cadr rec) t)
3675 dates)))))))
3676 dates)))
3677
3678(defun di:mark-entries ()
3679 "Mark calendar dates for iCalendar data from a file.
3680
3681This function allows you to mark the dates in an iCalendar-formatted
3682file in the calendar without importing it. The data is read directly
3683from the current value of `diary-file' (which is dynamically bound by
3684the diary when this function is called).
3685
3686To use this function, add an '#include \"FILE\"' entry in your diary
3687file for each iCalendar file you want to display (see
3688`diary-include-string'). Then add `diary-mark-included-diary-files' to
3689`diary-mark-entries-hook'. Finally, add this function to
3690`diary-nongregorian-marking-hook', so that it is called once for each
3691included file when dates are marked in the calendar."
3692 (with-no-warnings (defvar diary-file)) ; dyn. bound to included file name
3693 (let ((diary-buffer (or (find-buffer-visiting diary-file)
3694 (find-file-noselect diary-file))))
3695 (when (ical:contains-vcalendar-p diary-buffer)
3696 (let ((vcal/idx (ical:parse-and-index diary-buffer)))
3697 (when vcal/idx
3698 (let* ((index (cadr vcal/idx))
3699 (vcalendar (car vcal/idx))
3700 (to-mark
3701 (append (ical:ast-node-children-of 'ical:vevent vcalendar)
3702 (ical:ast-node-children-of 'ical:vjournal vcalendar)
3703 (ical:ast-node-children-of 'ical:vtodo vcalendar)))
3704 (all-dates (mapcan (lambda (c) (di:marking-dates-of c index))
3705 to-mark))
3706 (dates (seq-uniq
3707 (sort all-dates :lessp #'ical:date< :in-place t))))
3708
3709 (dolist (date dates)
3710 (let ((month (calendar-extract-month date))
3711 (year (calendar-extract-year date)))
3712 ;; avoid marking outside the displayed months,
3713 ;; to speed things up:
3714 (with-current-buffer calendar-buffer
3715 (with-suppressed-warnings
3716 ((free-vars displayed-year
3717 displayed-month))
3718 (when (and (= year displayed-year)
3719 (<= (1- displayed-month) month)
3720 (<= month (1+ displayed-month)))
3721 (calendar-mark-visible-date date))))))))))))
3722
3723
3724
3725(provide 'diary-icalendar)
3726
3727;; Local Variables:
3728;; read-symbol-shorthands: (("ical:" . "icalendar-") ("icr:" . "icalendar-recur-") ("di:" . "diary-icalendar-"))
3729;; End:
3730;;; diary-icalendar.el ends here
diff --git a/lisp/calendar/diary-lib.el b/lisp/calendar/diary-lib.el
index 36f9b0ef13b..a7ae6532287 100644
--- a/lisp/calendar/diary-lib.el
+++ b/lisp/calendar/diary-lib.el
@@ -38,15 +38,13 @@
38(defcustom diary-include-string "#include" 38(defcustom diary-include-string "#include"
39 "The string indicating inclusion of another file of diary entries. 39 "The string indicating inclusion of another file of diary entries.
40See the documentation for the function `diary-include-other-diary-files'." 40See the documentation for the function `diary-include-other-diary-files'."
41 :type 'string 41 :type 'string)
42 :group 'diary)
43 42
44(defcustom diary-list-include-blanks nil 43(defcustom diary-list-include-blanks nil
45 "If nil, do not include days with no diary entry in the list of diary entries. 44 "If nil, do not include days with no diary entry in the list of diary entries.
46Such days will then not be shown in the fancy diary buffer, even if they 45Such days will then not be shown in the fancy diary buffer, even if they
47are holidays." 46are holidays."
48 :type 'boolean 47 :type 'boolean)
49 :group 'diary)
50 48
51(defface diary-anniversary '((t :inherit font-lock-keyword-face)) 49(defface diary-anniversary '((t :inherit font-lock-keyword-face))
52 "Face used for anniversaries in the fancy diary display." 50 "Face used for anniversaries in the fancy diary display."
@@ -105,29 +103,24 @@ are: `string', `symbol', `int', `tnil', `stringtnil'."
105 (const :value int :tag "An integer") 103 (const :value int :tag "An integer")
106 (const :value tnil :tag "t or nil") 104 (const :value tnil :tag "t or nil")
107 (const :value stringtnil 105 (const :value stringtnil
108 :tag "A string, t, or nil")))) 106 :tag "A string, t, or nil")))))
109 :group 'diary)
110 107
111(defcustom diary-glob-file-regexp-prefix "^#" 108(defcustom diary-glob-file-regexp-prefix "^#"
112 "Regular expression prepended to `diary-face-attrs' for file-wide specifiers." 109 "Regular expression prepended to `diary-face-attrs' for file-wide specifiers."
113 :type 'regexp 110 :type 'regexp)
114 :group 'diary)
115 111
116(defcustom diary-file-name-prefix nil 112(defcustom diary-file-name-prefix nil
117 "Non-nil means prefix each diary entry with the name of the file defining it." 113 "Non-nil means prefix each diary entry with the name of the file defining it."
118 :type 'boolean 114 :type 'boolean)
119 :group 'diary)
120 115
121(defcustom diary-file-name-prefix-function #'identity 116(defcustom diary-file-name-prefix-function #'identity
122 "The function that will take a diary file name and return the desired prefix." 117 "The function that will take a diary file name and return the desired prefix."
123 :type 'function 118 :type 'function)
124 :group 'diary)
125 119
126(defcustom diary-sexp-entry-symbol "%%" 120(defcustom diary-sexp-entry-symbol "%%"
127 "The string used to indicate a sexp diary entry in `diary-file'. 121 "The string used to indicate a sexp diary entry in `diary-file'.
128See the documentation for the function `diary-list-sexp-entries'." 122See the documentation for the function `diary-list-sexp-entries'."
129 :type 'string 123 :type 'string)
130 :group 'diary)
131 124
132(defcustom diary-comment-start nil 125(defcustom diary-comment-start nil
133 "String marking the start of a comment in the diary, or nil. 126 "String marking the start of a comment in the diary, or nil.
@@ -138,24 +131,21 @@ for whatever you like, e.g. for meta-data that packages such as
138can be only one comment on any line. 131can be only one comment on any line.
139See also `diary-comment-end'." 132See also `diary-comment-end'."
140 :version "24.1" 133 :version "24.1"
141 :type '(choice (const :tag "No comment" nil) string) 134 :type '(choice (const :tag "No comment" nil) string))
142 :group 'diary)
143 135
144(defcustom diary-comment-end "" 136(defcustom diary-comment-end ""
145 "String marking the end of a comment in the diary. 137 "String marking the end of a comment in the diary.
146The empty string means comments finish at the end of a line. 138The empty string means comments finish at the end of a line.
147See also `diary-comment-start'." 139See also `diary-comment-start'."
148 :version "24.1" 140 :version "24.1"
149 :type 'string 141 :type 'string)
150 :group 'diary)
151 142
152(defcustom diary-hook nil 143(defcustom diary-hook nil
153 "Hook run after displaying the diary. 144 "Hook run after displaying the diary.
154Used for example by the appointment package - see `appt-activate'. 145Used for example by the appointment package - see `appt-activate'.
155The variables `number' and `original-date' are dynamically bound around 146The variables `number' and `original-date' are dynamically bound around
156the call." 147the call."
157 :type 'hook 148 :type 'hook)
158 :group 'diary)
159 149
160(defcustom diary-display-function #'diary-fancy-display 150(defcustom diary-display-function #'diary-fancy-display
161 "Function used to display the diary. 151 "Function used to display the diary.
@@ -171,10 +161,9 @@ holidays), or hard copy output."
171 (const :tag "Basic display" diary-simple-display) 161 (const :tag "Basic display" diary-simple-display)
172 (const :tag "No display" ignore) 162 (const :tag "No display" ignore)
173 (function :tag "User-specified function")) 163 (function :tag "User-specified function"))
174 :initialize 'custom-initialize-default 164 :initialize #'custom-initialize-default
175 :set 'diary-set-maybe-redraw 165 :set #'diary-set-maybe-redraw
176 :version "23.2" ; simple->fancy 166 :version "23.2") ; simple->fancy
177 :group 'diary)
178 167
179(defcustom diary-list-entries-hook nil 168(defcustom diary-list-entries-hook nil
180 "Hook run after diary file is culled for relevant entries. 169 "Hook run after diary file is culled for relevant entries.
@@ -201,8 +190,7 @@ So for example, to sort the complete list of diary entries you would
201use the list-entries hook, whereas to process e.g. Islamic entries in 190use the list-entries hook, whereas to process e.g. Islamic entries in
202the main file and all included files, you would use the nongregorian hook." 191the main file and all included files, you would use the nongregorian hook."
203 :type 'hook 192 :type 'hook
204 :options '(diary-include-other-diary-files diary-sort-entries) 193 :options '(diary-include-other-diary-files diary-sort-entries))
205 :group 'diary)
206 194
207(defcustom diary-mark-entries-hook nil 195(defcustom diary-mark-entries-hook nil
208 "List of functions called after marking diary entries in the calendar. 196 "List of functions called after marking diary entries in the calendar.
@@ -218,8 +206,7 @@ differ only if you are using included diary files. In that case,
218`displayed-year' and `displayed-month' are dynamically bound when 206`displayed-year' and `displayed-month' are dynamically bound when
219this hook is called." 207this hook is called."
220 :type 'hook 208 :type 'hook
221 :options '(diary-mark-included-diary-files) 209 :options '(diary-mark-included-diary-files))
222 :group 'diary)
223 210
224(defcustom diary-nongregorian-listing-hook nil 211(defcustom diary-nongregorian-listing-hook nil
225 "List of functions called for listing diary file and included files. 212 "List of functions called for listing diary file and included files.
@@ -236,8 +223,7 @@ use `diary-list-entries-hook', which runs only for the main diary file."
236 :options '(diary-bahai-list-entries 223 :options '(diary-bahai-list-entries
237 diary-hebrew-list-entries 224 diary-hebrew-list-entries
238 diary-islamic-list-entries 225 diary-islamic-list-entries
239 diary-chinese-list-entries) 226 diary-chinese-list-entries))
240 :group 'diary)
241 227
242(defcustom diary-nongregorian-marking-hook nil 228(defcustom diary-nongregorian-marking-hook nil
243 "List of functions called for marking diary file and included files. 229 "List of functions called for marking diary file and included files.
@@ -254,8 +240,7 @@ use `diary-mark-entries-hook', which runs only for the main diary file."
254 :options '(diary-bahai-mark-entries 240 :options '(diary-bahai-mark-entries
255 diary-hebrew-mark-entries 241 diary-hebrew-mark-entries
256 diary-islamic-mark-entries 242 diary-islamic-mark-entries
257 diary-chinese-mark-entries) 243 diary-chinese-mark-entries))
258 :group 'diary)
259 244
260(defcustom diary-print-entries-hook #'lpr-buffer 245(defcustom diary-print-entries-hook #'lpr-buffer
261 "Run by `diary-print-entries' after preparing a temporary diary buffer. 246 "Run by `diary-print-entries' after preparing a temporary diary buffer.
@@ -264,8 +249,7 @@ diary buffer. The default just does the printing. Other uses
264might include, for example, rearranging the lines into order by 249might include, for example, rearranging the lines into order by
265day and time, saving the buffer instead of deleting it, or 250day and time, saving the buffer instead of deleting it, or
266changing the function used to do the printing." 251changing the function used to do the printing."
267 :type 'hook 252 :type 'hook)
268 :group 'diary)
269 253
270(defcustom diary-unknown-time -9999 254(defcustom diary-unknown-time -9999
271 "Value returned by `diary-entry-time' when no time is found. 255 "Value returned by `diary-entry-time' when no time is found.
@@ -273,19 +257,16 @@ The default value -9999 causes entries with no recognizable time
273to be placed before those with times; 9999 would place entries 257to be placed before those with times; 9999 would place entries
274with no recognizable time after those with times." 258with no recognizable time after those with times."
275 :type 'integer 259 :type 'integer
276 :group 'diary
277 :version "20.3") 260 :version "20.3")
278 261
279(defcustom diary-mail-addr 262(defcustom diary-mail-addr
280 (or (bound-and-true-p user-mail-address) "") 263 (or (bound-and-true-p user-mail-address) "")
281 "Email address that `diary-mail-entries' will send email to." 264 "Email address that `diary-mail-entries' will send email to."
282 :group 'diary
283 :type 'string 265 :type 'string
284 :version "20.3") 266 :version "20.3")
285 267
286(defcustom diary-mail-days 7 268(defcustom diary-mail-days 7
287 "Default number of days for `diary-mail-entries' to check." 269 "Default number of days for `diary-mail-entries' to check."
288 :group 'diary
289 :type 'integer 270 :type 'integer
290 :version "20.3") 271 :version "20.3")
291 272
@@ -302,8 +283,7 @@ Used by the function `diary-remind', a pseudo-pattern is a list of
302expressions that can involve the keywords `days' (a number), `date' 283expressions that can involve the keywords `days' (a number), `date'
303\(a list of month, day, year), and `diary-entry' (a string)." 284\(a list of month, day, year), and `diary-entry' (a string)."
304 :type 'sexp 285 :type 'sexp
305 :risky t 286 :risky t)
306 :group 'diary)
307 287
308(defcustom diary-abbreviated-year-flag t 288(defcustom diary-abbreviated-year-flag t
309 "Interpret a two-digit year DD in a diary entry as either 19DD or 20DD. 289 "Interpret a two-digit year DD in a diary entry as either 19DD or 20DD.
@@ -312,8 +292,7 @@ When the current century is added to a two-digit year, if the result
312is more than 50 years in the future, the previous century is assumed. 292is more than 50 years in the future, the previous century is assumed.
313If the result is more than 50 years in the past, the next century is assumed. 293If the result is more than 50 years in the past, the next century is assumed.
314If this variable is nil, years must be written in full." 294If this variable is nil, years must be written in full."
315 :type 'boolean 295 :type 'boolean)
316 :group 'diary)
317 296
318(defun diary-outlook-format-1 (body) 297(defun diary-outlook-format-1 (body)
319 "Return a replace-match template for an element of `diary-outlook-formats'. 298 "Return a replace-match template for an element of `diary-outlook-formats'.
@@ -378,8 +357,7 @@ template following the rules above."
378 (string :tag "Template for entry") 357 (string :tag "Template for entry")
379 (function :tag 358 (function :tag
380 "Unary function providing template"))) 359 "Unary function providing template")))
381 :version "22.1" 360 :version "22.1")
382 :group 'diary)
383 361
384(defvar diary-header-line-flag) 362(defvar diary-header-line-flag)
385(defvar diary-header-line-format) 363(defvar diary-header-line-format)
@@ -401,10 +379,9 @@ template following the rules above."
401(defcustom diary-header-line-flag t 379(defcustom diary-header-line-flag t
402 "Non-nil means `diary-simple-display' will show a header line. 380 "Non-nil means `diary-simple-display' will show a header line.
403The format of the header is specified by `diary-header-line-format'." 381The format of the header is specified by `diary-header-line-format'."
404 :group 'diary
405 :type 'boolean 382 :type 'boolean
406 :initialize 'custom-initialize-default 383 :initialize #'custom-initialize-default
407 :set 'diary-set-header 384 :set #'diary-set-header
408 :version "22.1") 385 :version "22.1")
409 386
410(defvar diary-selective-display nil 387(defvar diary-selective-display nil
@@ -418,11 +395,10 @@ The format of the header is specified by `diary-header-line-format'."
418 ?\s (window-width))) 395 ?\s (window-width)))
419 "Format of the header line displayed by `diary-simple-display'. 396 "Format of the header line displayed by `diary-simple-display'.
420Only used if `diary-header-line-flag' is non-nil." 397Only used if `diary-header-line-flag' is non-nil."
421 :group 'diary
422 :type 'sexp 398 :type 'sexp
423 :risky t 399 :risky t
424 :initialize 'custom-initialize-default 400 :initialize #'custom-initialize-default
425 :set 'diary-set-header 401 :set #'diary-set-header
426 :version "23.3") ; frame-width -> window-width 402 :version "23.3") ; frame-width -> window-width
427 403
428;; The first version of this also checked for diary-selective-display 404;; The first version of this also checked for diary-selective-display
@@ -480,9 +456,8 @@ of days of diary entries displayed."
480 (integer :tag "Thursday") 456 (integer :tag "Thursday")
481 (integer :tag "Friday") 457 (integer :tag "Friday")
482 (integer :tag "Saturday"))) 458 (integer :tag "Saturday")))
483 :initialize 'custom-initialize-default 459 :initialize #'custom-initialize-default
484 :set 'diary-set-maybe-redraw 460 :set #'diary-set-maybe-redraw)
485 :group 'diary)
486 461
487;;; More user options in calendar.el, holidays.el. 462;;; More user options in calendar.el, holidays.el.
488 463
@@ -1443,9 +1418,9 @@ marks. This is intended to deal with deleted diary entries."
1443 (entry entry)) 1418 (entry entry))
1444 (if calendar-debug-sexp 1419 (if calendar-debug-sexp
1445 (let ((debug-on-error t)) 1420 (let ((debug-on-error t))
1446 (eval (car (read-from-string sexp)))) 1421 (eval (car (read-from-string sexp)) t))
1447 (condition-case err 1422 (condition-case err
1448 (eval (car (read-from-string sexp))) 1423 (eval (car (read-from-string sexp)) t)
1449 (error 1424 (error
1450 (display-warning 1425 (display-warning
1451 'diary 1426 'diary
@@ -1671,7 +1646,7 @@ be used instead of a colon (:) to separate the hour and minute parts."
1671If you add this function to `diary-list-entries-hook', it should 1646If you add this function to `diary-list-entries-hook', it should
1672be the last item in the hook, in case earlier items add diary 1647be the last item in the hook, in case earlier items add diary
1673entries, or change the order." 1648entries, or change the order."
1674 (setq diary-entries-list (sort diary-entries-list 'diary-entry-compare))) 1649 (setq diary-entries-list (sort diary-entries-list #'diary-entry-compare)))
1675 1650
1676 1651
1677(defun diary-list-sexp-entries (date) 1652(defun diary-list-sexp-entries (date)
@@ -2027,7 +2002,7 @@ Entry applies if the date is DAYS days after another diary-sexp SEXP."
2027 (user-error "Days must be an integer")) 2002 (user-error "Days must be an integer"))
2028 (let ((date (calendar-gregorian-from-absolute 2003 (let ((date (calendar-gregorian-from-absolute
2029 (- (calendar-absolute-from-gregorian date) days)))) 2004 (- (calendar-absolute-from-gregorian date) days))))
2030 (eval sexp))) 2005 (eval sexp t)))
2031 2006
2032(defun diary-day-of-year () 2007(defun diary-day-of-year ()
2033 "Day of year and number of days remaining in the year of date diary entry." 2008 "Day of year and number of days remaining in the year of date diary entry."
@@ -2058,7 +2033,7 @@ calendar."
2058 (and (integerp days) 2033 (and (integerp days)
2059 (< days 0) 2034 (< days 0)
2060 (setq days (number-sequence 1 (- days)))) 2035 (setq days (number-sequence 1 (- days))))
2061 (calendar-dlet ((diary-entry (eval sexp))) 2036 (calendar-dlet ((diary-entry (eval sexp t)))
2062 (cond 2037 (cond
2063 ;; Diary entry applies on date. 2038 ;; Diary entry applies on date.
2064 ((and diary-entry 2039 ((and diary-entry
@@ -2071,7 +2046,7 @@ calendar."
2071 ;; Adjust date, and re-evaluate. 2046 ;; Adjust date, and re-evaluate.
2072 (let ((date (calendar-gregorian-from-absolute 2047 (let ((date (calendar-gregorian-from-absolute
2073 (+ (calendar-absolute-from-gregorian date) days)))) 2048 (+ (calendar-absolute-from-gregorian date) days))))
2074 (when (setq diary-entry (eval sexp)) 2049 (when (setq diary-entry (eval sexp t))
2075 ;; Discard any mark portion from diary-anniversary, etc. 2050 ;; Discard any mark portion from diary-anniversary, etc.
2076 (if (consp diary-entry) (setq diary-entry (cdr diary-entry))) 2051 (if (consp diary-entry) (setq diary-entry (cdr diary-entry)))
2077 (calendar-dlet ((days days)) 2052 (calendar-dlet ((days days))
@@ -2120,8 +2095,9 @@ show the diary buffer."
2120Prefix argument ARG makes the entry nonmarking." 2095Prefix argument ARG makes the entry nonmarking."
2121 (interactive 2096 (interactive
2122 (list current-prefix-arg last-nonmenu-event)) 2097 (list current-prefix-arg last-nonmenu-event))
2123 (diary-make-entry (calendar-date-string (calendar-cursor-to-date t event) t t) 2098 (calendar-dlet ((calendar-date-display-form diary-date-insertion-form))
2124 arg)) 2099 (diary-make-entry (calendar-date-string (calendar-cursor-to-date t event) t t)
2100 arg)))
2125 2101
2126;;;###cal-autoload 2102;;;###cal-autoload
2127(defun diary-insert-weekly-entry (arg) 2103(defun diary-insert-weekly-entry (arg)
@@ -2299,7 +2275,7 @@ full month names."
2299 "") 2275 "")
2300 ;; With backup, last item is not part of date. 2276 ;; With backup, last item is not part of date.
2301 (if (equal (car x) 'backup) 2277 (if (equal (car x) 'backup)
2302 (concat "\\)" (eval (car (reverse x)))) 2278 (concat "\\)" (eval (car (reverse x)) t))
2303 "\\)")) 2279 "\\)"))
2304 '(1 'diary))) 2280 '(1 'diary)))
2305 diary-date-forms))) 2281 diary-date-forms)))
@@ -2318,6 +2294,10 @@ return a font-lock pattern matching array of MONTHS and marking SYMBOL."
2318 ;; Accepted formats: 10:00 10.00 10h00 10h 10am 10:00am 10.00am 2294 ;; Accepted formats: 10:00 10.00 10h00 10h 10am 10:00am 10.00am
2319 ;; Use of "." as a separator annoyingly matches numbers, eg "123.45". 2295 ;; Use of "." as a separator annoyingly matches numbers, eg "123.45".
2320 ;; Hence often prefix this with "\\(^\\|\\s-\\)." 2296 ;; Hence often prefix this with "\\(^\\|\\s-\\)."
2297 ;; FIXME: this regexp is too liberal to be used for parsing times from
2298 ;; entries by `diary-icalendar-parse-time', hence the existence of
2299 ;; `diary-icalendar-time-regexp'. Can we tighten it up so we don't
2300 ;; need both?
2321 (concat "[0-9]?[0-9]\\([AaPp][mM]\\|\\(" 2301 (concat "[0-9]?[0-9]\\([AaPp][mM]\\|\\("
2322 "[Hh]\\([0-9][0-9]\\)?\\|[:.][0-9][0-9]" 2302 "[Hh]\\([0-9][0-9]\\)?\\|[:.][0-9][0-9]"
2323 "\\)\\([AaPp][Mm]\\)?\\)") 2303 "\\)\\([AaPp][Mm]\\)?\\)")
diff --git a/lisp/calendar/icalendar-ast.el b/lisp/calendar/icalendar-ast.el
new file mode 100644
index 00000000000..e9c289f16db
--- /dev/null
+++ b/lisp/calendar/icalendar-ast.el
@@ -0,0 +1,957 @@
1;;; icalendar-ast.el --- Syntax trees for iCalendar -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Free Software Foundation, Inc.
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: October 2024
7;; Keywords: calendar
8;; Human-Keywords: calendar, iCalendar
9
10;; This file is part of GNU Emacs.
11
12;; This file is free software: you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; This file is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with this file. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; This file defines the abstract syntax tree representation for
28;; iCalendar data. The AST is based on `org-element-ast' (which see;
29;; that feature will eventually be renamed and moved out of the Org tree
30;; into the main tree).
31
32;; This file contains low-level functions for constructing and
33;; manipulating the AST, most of which are minimal wrappers around the
34;; functions provided by `org-element-ast'. This low-level API is
35;; primarily used by `icalendar-parser'. It also contains a higher-level
36;; API for constructing AST nodes in Lisp code. Finally, it defines
37;; functions for validating AST nodes.
38
39;; There are three main pieces of data in an AST node: its type, its
40;; value, and its child nodes. Nodes which represent iCalendar
41;; components have no values; they are simply containers for their
42;; children. Nodes which represent data of the base iCalendar data
43;; types have no children; they are the leaf nodes in the syntax tree.
44;; The main low-level accessors for these data in AST nodes are:
45;;
46;; `icalendar-ast-node-type'
47;; `icalendar-ast-node-value'
48;; `icalendar-ast-node-children'
49;; `icalendar-ast-node-children-of'
50;; `icalendar-ast-node-first-child-of'
51
52;; To construct AST nodes in Lisp code, see especially the high-level macros:
53;;
54;; `icalendar-make-vcalendar'
55;; `icalendar-make-vtimezone'
56;; `icalendar-make-vevent'
57;; `icalendar-make-vtodo'
58;; `icalendar-make-vjournal'
59;; `icalendar-make-property'
60;; `icalendar-make-param'
61;;
62;; These macros wrap the macro `icalendar-make-node-from-templates',
63;; which allows writing iCalendar syntax tree nodes as Lisp templates.
64
65;; Constructing nodes with these macros automatically validates them
66;; with the function `icalendar-ast-node-valid-p', which signals an
67;; `icalendar-validation-error' if the node is not valid acccording to
68;; RFC5545.
69
70
71;;; Code:
72(eval-when-compile (require 'icalendar-macs))
73(require 'icalendar)
74(require 'org-element-ast)
75(require 'cl-lib)
76
77;;; Type symbols and metadata
78
79;; All nodes in the syntax tree have a type symbol as their first element.
80;; We use the following symbol properties (all prefixed with 'icalendar-')
81;; to associate type symbols with various important data about the type:
82;;
83;; is-type - t (marks this symbol as an icalendar type)
84;; is-value, is-param, is-property, or is-component - t
85;; (specifies what sort of value this type represents)
86;; list-sep - for property and parameters types, a string (typically
87;; "," or ";") which separates individual printed values, if the
88;; type allows lists of values. If this is non-nil, syntax nodes of
89;; this type should always have a list of values in their VALUE
90;; field (even if there is only one value)
91;; matcher - a function to match this type. This function matches the
92;; regular expression defined under the type's name; it is used to provide
93;; syntax highlighting in `icalendar-mode'
94;; begin-rx, end-rx - for component-types, an `rx' regular expression which
95;; matches the BEGIN and END lines that form its boundaries
96;; value-rx - an `rx' regular expression which matches individual values
97;; of this type, with no consideration for quoting or lists of values.
98;; (For value types, this is just a synonym for the rx definition
99;; under the type's symbol)
100;; values-rx - for types that accept lists of values, an `rx' regular
101;; expression which matches the whole list (including quotes, if required)
102;; full-value-rx - for property and parameter types, an `rx' regular
103;; expression which matches a valid value expression in group 2, or
104;; an invalid value in group 3
105;; value-reader - for value types, a function which creates syntax
106;; nodes of this type given a string representing their value
107;; value-printer - for value types, a function to print individual
108;; values of this type. It accepts a value and returns its string
109;; representation.
110;; default-value - for property and parameter types, a string
111;; representing a default value for nodes of this type. This is the
112;; value assumed when no node of this type is present in the
113;; relevant part of the syntax tree.
114;; substitute-value - for parameter types, a string representing a value
115;; which will be substituted at parse times for unrecognized values.
116;; (This is normally the same as default-value, but differs from it
117;; in at least one case in RFC5545, thus it is stored separately.)
118;; default-type - for property types which can accept values of multiple
119;; types, this is the default type when no type for the value is
120;; specified in the parameters. Any type of value other than this
121;; one requires a VALUE=... parameter when the property is read or printed.
122;; other-types - for property types which can accept values of multiple types,
123;; this is a list of other types that the property can accept.
124;; value-type - for param types, this is the value type which the parameter
125;; can accept.
126;; child-spec - for property and component types, a plist describing the
127;; required and optional child nodes. See `icalendar-define-property' and
128;; `icalendar-define-component' for details.
129;; other-validator - a function to perform type-specific validation
130;; for nodes of this type. If present, this function will be called
131;; by `icalendar-ast-node-valid-p' during validation.
132;; type-documentation - a string documenting the type. This documentation is
133;; printed in the help buffer when `describe-symbol' is called on TYPE.
134;; link - a hyperlink to the documentation of the type in the relevant standard
135
136(defun ical:type-symbol-p (symbol)
137 "Return non-nil if SYMBOL is an iCalendar type symbol.
138
139This function only checks that SYMBOL has been marked as a type;
140it returns t for value types defined by `icalendar-define-type',
141but also e.g. for types defined by `icalendar-define-param' and
142`icalendar-define-property'. To check that SYMBOL names a value
143type for property or parameter values, see
144`icalendar-value-type-symbol-p' and
145`icalendar-printable-value-type-symbol-p'."
146 (and (symbolp symbol)
147 (get symbol 'ical:is-type)))
148
149(defun ical:value-type-symbol-p (symbol)
150 "Return non-nil if SYMBOL is a type symbol for a value type.
151
152This means that SYMBOL must both satisfy `icalendar-type-symbol-p' and
153have the property `icalendar-is-value'. It does not require the type to
154be associated with a print name in `icalendar-value-types'; for that see
155`icalendar-printable-value-type-symbol-p'."
156 (and (ical:type-symbol-p symbol)
157 (get symbol 'ical:is-value)))
158
159(defun ical:expects-list-of-values-p (type)
160 "Return non-nil if TYPE expects a list of values.
161
162This is never t for value types or component types. For property and
163parameter types defined with `icalendar-define-param' and
164`icalendar-define-property', it is true if the :list-sep argument was
165specified in the definition."
166 (and (ical:type-symbol-p type)
167 (get type 'ical:list-sep)))
168
169(defun ical:param-type-symbol-p (type)
170 "Return non-nil if TYPE is a type symbol for an iCalendar parameter."
171 (and (ical:type-symbol-p type)
172 (get type 'ical:is-param)))
173
174(defun ical:property-type-symbol-p (type)
175 "Return non-nil if TYPE is a type symbol for an iCalendar property."
176 (and (ical:type-symbol-p type)
177 (get type 'ical:is-property)))
178
179(defun ical:component-type-symbol-p (type)
180 "Return non-nil if TYPE is a type symbol for an iCalendar component."
181 (and (ical:type-symbol-p type)
182 (get type 'ical:is-component)))
183
184;; TODO: we could define other accessors here for the other metadata
185;; properties, but at the moment I see no advantage to this; they would
186;; all just be long-winded wrappers around `get'.
187
188
189;; The basic, low-level API for the AST, mostly intended for use by
190;; `icalendar-parser'. These functions are mostly aliases and simple
191;; wrappers around functions provided by `org-element-ast', which does
192;; the heavy lifting.
193(defalias 'ical:ast-node-type #'org-element-type)
194
195(defsubst ical:ast-node-value (node)
196 "Return the value of iCalendar syntax node NODE.
197In component nodes, this is nil. Otherwise, it is a syntax node
198representing an iCalendar (property or parameter) value."
199 (org-element-property :value node))
200
201(defalias 'ical:ast-node-children #'org-element-contents)
202
203;; TODO: probably don't want &rest form for this
204(defalias 'ical:ast-node-set-children #'org-element-set-contents)
205
206(defalias 'ical:ast-node-adopt-children #'org-element-adopt-elements)
207
208(defalias 'ical:ast-node-meta-get #'org-element-property)
209
210(defalias 'ical:ast-node-meta-set #'org-element-put-property)
211
212(defun ical:ast-node-set-type (node type)
213 "Set the type of iCalendar syntax node NODE to TYPE.
214
215This function is probably not what you want! It directly modifies the
216type of NODE in-place, which could make the node invalid if its value or
217children do not match the new TYPE. If you do not know in advance that
218the data in NODE is compatible with the new TYPE, it is better to
219construct a new syntax node."
220 (setcar node type))
221
222(defun ical:ast-node-set-value (node value)
223 "Set the value of iCalendar syntax node NODE to VALUE."
224 (ical:ast-node-meta-set node :value value))
225
226(defun ical:make-ast-node (type props &optional children)
227 "Construct a syntax node of TYPE with meta-properties PROPS and CHILDREN.
228
229This is a low-level constructor. If you are constructing iCalendar
230syntax nodes directly in Lisp code, consider using one of the
231higher-level macros based on `icalendar-make-node-from-templates'
232instead, which expand to calls to this function but also perform type
233checking and validation.
234
235TYPE should be an iCalendar type symbol. CHILDREN, if given, should be
236a list of syntax nodes. In property nodes, these should be the
237parameters of the property. In component nodes, these should be the
238properties or subcomponents of the component. CHILDREN should otherwise
239be nil.
240
241PROPS should be a plist with any of the following keywords:
242
243:value - in value nodes, this should be the Elisp value parsed from a
244 property or parameter's value string. In parameter and property nodes,
245 this should be a value node or list of value nodes. In component
246 nodes, it should not be present.
247:buffer - buffer from which VALUE was parsed
248:begin - position at which this node begins in BUFFER
249:end - position at which this node ends in BUFFER
250:value-begin - position at which VALUE begins in BUFFER
251:value-end - position at which VALUE ends in BUFFER
252:original-value - a string containing the original, uninterpreted value
253 of the node. This can differ from (a string represented by) VALUE
254 if e.g. a default VALUE was substituted for an unrecognized but
255 syntactically correct value.
256:original-name - a string containing the original, uninterpreted name
257 of the parameter, property or component this node represents.
258 This can differ from (a string representing) TYPE
259 if e.g. a default TYPE was substituted for an unrecognized but
260 syntactically correct one."
261 ;; automatically mark :value as a "secondary property" for org-element-ast
262 (let ((full-props (if (plist-member props :value)
263 (plist-put props :secondary (list :value))
264 props)))
265 (apply #'org-element-create type full-props children)))
266
267(defun ical:ast-node-p (val)
268 "Return non-nil if VAL is an iCalendar syntax node."
269 (and (listp val)
270 (length> val 1)
271 (ical:type-symbol-p (ical:ast-node-type val))
272 (plistp (cadr val))
273 (listp (ical:ast-node-children val))))
274
275(defun ical:param-node-p (node)
276 "Return non-nil if NODE is a syntax node whose type is a parameter type."
277 (and (ical:ast-node-p node)
278 (ical:param-type-symbol-p (ical:ast-node-type node))))
279
280(defun ical:property-node-p (node)
281 "Return non-nil if NODE is a syntax node whose type is a property type."
282 (and (ical:ast-node-p node)
283 (ical:property-type-symbol-p (ical:ast-node-type node))))
284
285(defun ical:component-node-p (node)
286 "Return non-nil if NODE is a syntax node whose type is a component type."
287 (and (ical:ast-node-p node)
288 (ical:component-type-symbol-p (ical:ast-node-type node))))
289
290(defun ical:ast-node-first-child-of (type node)
291 "Return the first child of NODE of type TYPE, or nil."
292 (assq type (ical:ast-node-children node)))
293
294(defun ical:ast-node-children-of (type node)
295 "Return a list of all the children of NODE of type TYPE."
296 (seq-filter (lambda (c) (eq type (ical:ast-node-type c)))
297 (ical:ast-node-children node)))
298
299
300;; A high-level API for constructing iCalendar syntax nodes in Lisp code:
301(defun ical:type-of (value &optional types)
302 "Find the iCalendar type symbol for the type to which VALUE belongs.
303
304TYPES, if specified, should be a list of type symbols to check.
305TYPES defaults to all type symbols listed in `icalendar-value-types'."
306 (require 'icalendar-parser) ; for ical:value-types, ical:list-of-p
307 (declare-function ical:list-of-p "icalendar-parser")
308 (catch 'found
309 (when (ical:ast-node-p value)
310 (throw 'found (ical:ast-node-type value)))
311 ;; FIXME: the warning here is spurious, given that icalendar-parser
312 ;; is require'd above:
313 (with-suppressed-warnings ((free-vars ical:value-types))
314 (dolist (type (or types (mapcar #'cdr ical:value-types)))
315 (if (ical:expects-list-of-values-p type)
316 (when (ical:list-of-p value type)
317 (throw 'found type))
318 (when (cl-typep value type)
319 (throw 'found type)))))))
320
321;; A more flexible constructor for value nodes which can choose the
322;; correct type from a list. This helps keep templates succinct and easy
323;; to use in `icalendar-make-node-from-templates', and related macros
324;; below.
325(defun ical:make-value-node-of (type value)
326 "Make an iCalendar syntax node of type TYPE containing VALUE as its value.
327
328TYPE should be a symbol for an iCalendar value type, and VALUE should be
329a value of that type. If TYPE is the symbol \\='plain-text, VALUE should
330be a string, and in that case VALUE is returned as-is.
331
332TYPE may also be a list of type symbols; in that case, the first type in
333the list which VALUE satisfies is used as the returned node's type. If
334the list is nil, VALUE will be checked against all types in
335`icalendar-value-types'.
336
337If VALUE is nil, and `icalendar-boolean' is not (in) TYPE, nil is
338returned. Otherwise, a \\='wrong-type-argument error is signaled if
339VALUE does not satisfy (any type in) TYPE."
340 (require 'icalendar-parser) ; for `icalendar-list-of-p'
341 (cond
342 ((and (null value)
343 (not (if (listp type) (memq 'ical:boolean type)
344 (eq 'ical:boolean type))))
345 ;; Instead of signaling an error, we just return nil in this case.
346 ;; This allows the `ical:make-*' macros higher up the stack to
347 ;; filter out templates that evaluate to nil at run time:
348 nil)
349 ((eq type 'plain-text)
350 (unless (stringp value)
351 (signal 'wrong-type-argument (list 'stringp value)))
352 value)
353 ((symbolp type)
354 (unless (ical:value-type-symbol-p type)
355 (signal 'wrong-type-argument (list 'icalendar-value-type-symbol-p type)))
356 (if (ical:expects-list-of-values-p type)
357 (unless (ical:list-of-p value type)
358 (signal 'wrong-type-argument (list `(list-of ,type) value)))
359 (unless (cl-typep value type)
360 (signal 'wrong-type-argument (list type value)))
361 (ical:make-ast-node type (list :value value))))
362 ((listp type)
363 ;; N.B. nil is allowed; in that case, `ical:type-of' will check all
364 ;; types in `ical:value-types':
365 (let ((the-type (ical:type-of value type)))
366 (if the-type
367 (ical:make-ast-node the-type (list :value value))
368 (signal 'wrong-type-argument
369 (list (if (length> type 1) (cons 'or type) (car type))
370 value)))))
371 (t (signal 'wrong-type-argument (list '(or symbolp listp) type)))))
372
373(defun ical:-make-param--list (type value-type raw-values)
374 "Make a param node of TYPE with list of values RAW-VALUES of type VALUE-TYPE."
375 (let ((value (if (seq-every-p #'ical:ast-node-p raw-values)
376 raw-values
377 (mapcar
378 (lambda (c)
379 (ical:make-value-node-of value-type c))
380 raw-values))))
381 (when value
382 (ical:ast-node-valid-p
383 (ical:make-ast-node
384 type
385 (list :value value))))))
386
387(defun ical:-make-param--nonlist (type value-type raw-value)
388 "Make a param node of TYPE with value RAW-VALUE of type VALUE-TYPE."
389 (let ((value (if (ical:ast-node-p raw-value)
390 raw-value
391 (ical:make-value-node-of value-type raw-value))))
392 (when value
393 (ical:ast-node-valid-p
394 (ical:make-ast-node
395 type
396 (list :value value))))))
397
398(defmacro ical:make-param (type value)
399 "Construct an iCalendar parameter node of TYPE with value VALUE.
400
401TYPE should be an iCalendar type symbol satisfying
402`icalendar-param-type-symbol-p'; it should not be quoted.
403
404VALUE should evaluate to a value appropriate for TYPE. In particular, if
405TYPE expects a list of values (see `icalendar-expects-list-p'), VALUE
406should be such a list. If necessary, the value(s) in VALUE will be
407wrapped in syntax nodes indicating their type.
408
409For example,
410
411 (icalendar-make-param icalendar-deltoparam
412 (list \"mailto:minionA@example.com\" \"mailto:minionB@example.com\"))
413
414will return an `icalendar-deltoparam' node whose value is a list of
415`icalendar-cal-address' nodes containing the two addresses.
416
417The resulting syntax node is checked for validity by
418`icalendar-ast-node-valid-p' before it is returned."
419 (declare (debug (symbolp form)))
420 ;; TODO: support `ical:otherparam'
421 (unless (ical:param-type-symbol-p type)
422 (error "Not an iCalendar param type: %s" type))
423 (let ((value-type (or (get type 'ical:value-type) 'plain-text)))
424 (if (ical:expects-list-of-values-p type)
425 `(ical:-make-param--list ',type ',value-type ,value)
426 `(ical:-make-param--nonlist ',type ',value-type ,value))))
427
428(defun ical:-make-property--list (type value-types raw-values &optional params)
429 "Make a property node of TYPE with list of values RAW-VALUES.
430VALUE-TYPES should be a list of value types that TYPE accepts.
431PARAMS, if given, should be a list of parameter nodes."
432 (require 'icalendar-parser) ; for `ical:maybe-add-value-param'
433 (declare-function ical:maybe-add-value-param "icalendar-parser")
434
435 (let ((value (if (seq-every-p #'ical:ast-node-p raw-values)
436 raw-values
437 (mapcar
438 (lambda (c) (ical:make-value-node-of value-types c))
439 raw-values))))
440 (when value
441 (ical:ast-node-valid-p
442 (ical:maybe-add-value-param
443 (ical:make-ast-node type (list :value value) params))))))
444
445(defun ical:-make-property--nonlist (type value-types raw-value &optional params)
446 "Make a property node of TYPE with value RAW-VALUE.
447VALUE-TYPES should be a list of value types that TYPE accepts.
448PARAMS, if given, should be a list of parameter nodes."
449 (require 'icalendar-parser) ; for `ical:maybe-add-value-param'
450 (declare-function ical:maybe-add-value-param "icalendar-parser")
451
452 (let ((value (if (ical:ast-node-p raw-value)
453 raw-value
454 (ical:make-value-node-of value-types raw-value))))
455 (when value
456 (ical:ast-node-valid-p
457 (ical:maybe-add-value-param
458 (ical:make-ast-node type (list :value value) params))))))
459
460(defmacro ical:make-property (type value &rest param-templates)
461 "Construct an iCalendar property node of TYPE with value VALUE.
462
463TYPE should be an iCalendar type symbol satisfying
464`icalendar-property-type-symbol-p'; it should not be quoted.
465
466VALUE should evaluate to a value appropriate for TYPE. In particular,
467if TYPE expects a list of values (see
468`icalendar-expects-list-of-values-p'), VALUE should be such a list. If
469necessary, the value(s) in VALUE will be wrapped in syntax nodes
470indicating their type. If VALUE is not of the default value type for
471TYPE, an `icalendar-valuetypeparam' will automatically be added to
472PARAM-TEMPLATES.
473
474Each element of PARAM-TEMPLATES should represent a parameter node; see
475`icalendar-make-node-from-templates' for the format of such templates.
476A template can also have the form (@ L), where L evaluates to a list of
477parameter nodes to be added to the component.
478
479PARAM-TEMPLATES which evaluate to nil are removed when the property node
480is constructed.
481
482For example,
483
484 (icalendar-make-property icalendar-rdate (list \\='(2 1 2025) \\='(3 1 2025)))
485
486will return an `icalendar-rdate' node whose value is a list of
487`icalendar-date' nodes containing the dates above as their values.
488
489The resulting syntax node is checked for validity by
490`icalendar-ast-node-valid-p' before it is returned."
491 ;; TODO: support `ical:other-property', maybe like
492 ;; (ical:other-property "X-NAME" value ...)
493 (declare (debug (symbolp form &rest form))
494 (indent 2))
495 (unless (ical:property-type-symbol-p type)
496 (error "Not an iCalendar property type: %s" type))
497 (let ((value-types (cons (get type 'ical:default-type)
498 (get type 'ical:other-types)))
499 params-expr children lists-of-children)
500 (dolist (c param-templates)
501 (cond ((and (listp c) (ical:type-symbol-p (car c)))
502 ;; c is a template for a child node, so it should be
503 ;; recursively expanded:
504 (push (cons 'ical:make-node-from-templates c)
505 children))
506 ((and (listp c) (eq '@ (car c)))
507 ;; c is a template (@ L) where L evaluates to a list of children:
508 (push (cadr c) lists-of-children))
509 (t
510 ;; otherwise, just pass c through as is; this allows
511 ;; interleaving templates with other expressions that
512 ;; evaluate to syntax nodes:
513 (push c children))))
514 (when (or children lists-of-children)
515 (setq params-expr
516 `(seq-filter #'identity
517 (append (list ,@children) ,@lists-of-children))))
518
519 (if (ical:expects-list-of-values-p type)
520 `(ical:-make-property--list ',type ',value-types ,value ,params-expr)
521 `(ical:-make-property--nonlist ',type ',value-types ,value ,params-expr))))
522
523(defmacro ical:make-component (type &rest templates)
524 "Construct an iCalendar component node of TYPE from TEMPLATES.
525
526TYPE should be an iCalendar type symbol satisfying
527`icalendar-component-type-symbol-p'; it should not be quoted.
528
529Each expression in TEMPLATES should represent a child node of the
530component; see `icalendar-make-node-from-templates' for the format of
531such TEMPLATES. A template can also have the form (@ L), where L
532evaluates to a list of child nodes to be added to the component.
533
534Any value in TEMPLATES that evaluates to nil will be removed before the
535component node is constructed.
536
537If TYPE is `icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal',
538or `icalendar-vfreebusy', the properties `icalendar-dtstamp' and
539`icalendar-uid' will be automatically provided, if they are absent in
540TEMPLATES. Likewise, if TYPE is `icalendar-vcalendar', the properties
541`icalendar-prodid', `icalendar-version', and `icalendar-calscale' will
542be automatically provided if absent.
543
544For example,
545
546 (icalendar-make-component icalendar-vevent
547 (icalendar-summary \"Party\")
548 (icalendar-location \"Robot House\")
549 (@ list-of-other-properties))
550
551will return an `icalendar-vevent' node containing the provided
552properties as well as `icalendar-dtstamp' and `icalendar-uid'
553properties.
554
555The resulting syntax node is checked for validity by
556`icalendar-ast-node-valid-p' before it is returned."
557 (declare (debug (symbolp &rest form))
558 (indent 1))
559 ;; TODO: support `ical:other-component', maybe like
560 ;; (ical:other-component (:x-name "X-NAME") templates ...)
561 (unless (ical:component-type-symbol-p type)
562 (error "Not an iCalendar component type: %s" type))
563 ;; Add templates for required properties automatically if we can:
564 (when (memq type '(ical:vevent ical:vtodo ical:vjournal ical:vfreebusy))
565 (unless (assq 'ical:dtstamp templates)
566 (push '(ical:dtstamp (decode-time nil t))
567 templates))
568 (unless (assq 'ical:uid templates)
569 (push `(ical:uid ,(ical:make-uid templates))
570 templates)))
571 (when (eq type 'ical:vcalendar)
572 (unless (assq 'ical:prodid templates)
573 (push `(ical:prodid ,ical:vcalendar-prodid)
574 templates))
575 (unless (assq 'ical:version templates)
576 (push `(ical:version ,ical:vcalendar-version)
577 templates))
578 (unless (assq 'ical:calscale templates)
579 (push '(ical:calscale "GREGORIAN")
580 templates)))
581 (when (null templates)
582 (error "At least one template is required"))
583
584 (let (children lists-of-children)
585 (dolist (c templates)
586 (cond ((and (listp c) (ical:type-symbol-p (car c)))
587 ;; c is a template for a child node, so it should be
588 ;; recursively expanded:
589 (push (cons 'ical:make-node-from-templates c)
590 children))
591 ((and (listp c) (eq '@ (car c)))
592 ;; c is a template (@ L) where L evaluates to a list of children:
593 (push (cadr c) lists-of-children))
594 (t
595 ;; otherwise, just pass c through as is; this allows
596 ;; interleaving templates with other expressions that
597 ;; evaluate to syntax nodes:
598 (push c children))))
599 (setq children (nreverse children)
600 lists-of-children (nreverse lists-of-children))
601 (when (or children lists-of-children)
602 `(ical:ast-node-valid-p
603 (ical:make-ast-node
604 (quote ,type)
605 nil
606 (seq-filter #'identity
607 (append (list ,@children) ,@lists-of-children)))))))
608
609;; TODO: allow disabling the validity check??
610(defmacro ical:make-node-from-templates (type &rest templates)
611 "Construct an iCalendar syntax node of TYPE from TEMPLATES.
612
613TYPE should be an iCalendar type symbol; it should not be quoted. This
614macro (and the derived macros `icalendar-make-vcalendar',
615`icalendar-make-vevent', `icalendar-make-vtodo',
616`icalendar-make-vjournal', `icalendar-make-vfreebusy',
617`icalendar-make-valarm', `icalendar-make-vtimezone',
618`icalendar-make-standard', and `icalendar-make-daylight') makes it easy
619to write iCalendar syntax nodes of TYPE as Lisp code.
620
621Each expression in TEMPLATES represents a child node of the constructed
622node. It must either evaluate to such a node, or it must have one of
623the following forms:
624
625\(VALUE-TYPE VALUE) - constructs a node of VALUE-TYPE containing the
626 value VALUE.
627
628\(PARAM-TYPE VALUE) - constructs a parameter node of PARAM-TYPE
629 containing the VALUE.
630
631\(PROPERTY-TYPE VALUE [PARAM ...]) - constructs a property node of
632 PROPERTY-TYPE containing the value VALUE and PARAMs as child
633 nodes. Each PARAM should be a template (PARAM-TYPE VALUE), as above,
634 or any other expression that evaluates to a parameter node.
635
636\(COMPONENT-TYPE CHILD [CHILD ...]) - constructs a component node of
637 COMPONENT-TYPE with CHILDs as child nodes. Each CHILD should either be
638 a template for a property (as above), a template for a
639 sub-component (of the same form), or any other expression that
640 evaluates to an iCalendar syntax node.
641
642If TYPE is an iCalendar component or property type, a TEMPLATE can also
643have the form (@ L), where L evaluates to a list of child nodes to be
644added to the component or property node.
645
646For example, an iCalendar VEVENT could be written like this:
647
648 (icalendar-make-node-from-templates icalendar-vevent
649 (icalendar-dtstamp (decode-time (current-time) 0))
650 (icalendar-uid \"some-unique-id\")
651 (icalendar-summary \"Party\")
652 (icalendar-location \"Robot House\")
653 (icalendar-organizer \"mailto:bender@mars.edu\")
654 (icalendar-attendee \"mailto:philip.j.fry@mars.edu\"
655 (icalendar-partstatparam \"ACCEPTED\"))
656 (icalendar-attendee \"mailto:gunther@mars.edu\"
657 (icalendar-partstatparam \"DECLINED\"))
658 (icalendar-categories (list \"MISCHIEF\" \"DOUBLE SECRET PROBATION\"))
659 (icalendar-dtstart (icalendar-make-date-time :year 3003 :month 3 :day 13
660 :hour 22 :minute 0 :second 0)
661 (icalendar-tzidparam \"Mars/University_Time\")))
662
663Before the constructed node is returned, it is validated by
664`icalendar-ast-node-valid-p'."
665 (declare (debug (symbolp &rest form))
666 (indent 1))
667 (cond
668 ((not (ical:type-symbol-p type))
669 (error "Not an iCalendar type symbol: %s" type))
670 ((ical:value-type-symbol-p type)
671 `(ical:ast-node-valid-p
672 (ical:make-value-node-of (quote ,type) ,(car templates))))
673 ((ical:param-type-symbol-p type)
674 `(ical:make-param ,type ,(car templates)))
675 ((ical:property-type-symbol-p type)
676 `(ical:make-property ,type ,(car templates) ,@(cdr templates)))
677 ((ical:component-type-symbol-p type)
678 `(ical:make-component ,type ,@templates))))
679
680(defmacro ical:make-vcalendar (&rest templates)
681 "Construct an iCalendar VCALENDAR object from TEMPLATES.
682See `icalendar-make-node-from-templates' for the format of TEMPLATES.
683See `icalendar-vcalendar' for the permissible child types.
684
685If TEMPLATES does not contain templates for the `icalendar-prodid' and
686`icalendar-version' properties, they will be automatically added; see
687the variables `icalendar-vcalendar-prodid' and
688`icalendar-vcalendar-version'."
689 `(ical:make-node-from-templates ical:vcalendar ,@templates))
690
691(defmacro ical:make-vevent (&rest templates)
692 "Construct an iCalendar VEVENT node from TEMPLATES.
693See `icalendar-make-node-from-templates' for the format of TEMPLATES.
694See `icalendar-vevent' for the permissible child types.
695
696If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
697`icalendar-uid' properties (both required), they will be automatically
698provided."
699 `(ical:make-node-from-templates ical:vevent ,@templates))
700
701(defmacro ical:make-vtodo (&rest templates)
702 "Construct an iCalendar VTODO node from TEMPLATES.
703See `icalendar-make-node-from-templates' for the format of TEMPLATES.
704See `icalendar-vtodo' for the permissible child types.
705
706If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
707`icalendar-uid' properties (both required), they will be automatically
708provided."
709 `(ical:make-node-from-templates ical:vtodo ,@templates))
710
711(defmacro ical:make-vjournal (&rest templates)
712 "Construct an iCalendar VJOURNAL node from TEMPLATES.
713See `icalendar-make-node-from-templates' for the format of TEMPLATES.
714See `icalendar-vjournal' for the permissible child types.
715
716If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
717`icalendar-uid' properties (both required), they will be automatically
718provided."
719 `(ical:make-node-from-templates ical:vjournal ,@templates))
720
721(defmacro ical:make-vfreebusy (&rest templates)
722 "Construct an iCalendar VFREEBUSY node from TEMPLATES.
723See `icalendar-make-node-from-templates' for the format of TEMPLATES.
724See `icalendar-vfreebusy' for the permissible child types.
725
726If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
727`icalendar-uid' properties (both required), they will be automatically
728provided."
729 `(ical:make-node-from-templates ical:vfreebusy ,@templates))
730
731(defmacro ical:make-valarm (&rest templates)
732 "Construct an iCalendar VALARM node from TEMPLATES.
733See `icalendar-make-node-from-templates' for the format of TEMPLATES.
734See `icalendar-valarm' for the permissible child types."
735 `(ical:make-node-from-templates ical:valarm ,@templates))
736
737(defmacro ical:make-vtimezone (&rest templates)
738 "Construct an iCalendar VTIMEZONE node from TEMPLATES.
739See `icalendar-make-node-from-templates' for the format of TEMPLATES.
740See `icalendar-vtimezone' for the permissible child types."
741 `(ical:make-node-from-templates ical:vtimezone ,@templates))
742
743(defmacro ical:make-standard (&rest templates)
744 "Construct an iCalendar STANDARD node from TEMPLATES.
745See `icalendar-make-node-from-templates' for the format of TEMPLATES.
746See `icalendar-standard' for the permissible child types."
747 `(ical:make-node-from-templates ical:standard ,@templates))
748
749(defmacro ical:make-daylight (&rest templates)
750 "Construct an iCalendar DAYLIGHT node from TEMPLATES.
751See `icalendar-make-node-from-templates' for the format of TEMPLATES.
752See `icalendar-daylight' for the permissible child types."
753 `(ical:make-node-from-templates ical:daylight ,@templates))
754
755
756;;; Validation:
757
758;; Errors at the validation stage:
759;; e.g. property/param values did not match, or are of the wrong type,
760;; or required properties not present in a component
761(define-error 'ical:validation-error "Invalid iCalendar data" 'ical:error)
762
763(cl-defun ical:signal-validation-error (msg &key node (severity 2))
764 (signal 'ical:validation-error
765 (list :message msg
766 :buffer (ical:ast-node-meta-get :buffer node)
767 :position (ical:ast-node-meta-get :begin node)
768 :severity severity
769 :node node)))
770
771(defun ical:ast-node-required-child-p (child parent)
772 "Return non-nil if CHILD is required by PARENT's node type."
773 (let* ((type (ical:ast-node-type parent))
774 (child-spec (get type 'ical:child-spec))
775 (child-type (ical:ast-node-type child)))
776 (or (memq child-type (plist-get child-spec :one))
777 (memq child-type (plist-get child-spec :one-or-more)))))
778
779(defun ical:ast-node-valid-value-p (node)
780 "Validate that NODE's value satisfies the requirements of its type.
781Signals an `icalendar-validation-error' if NODE's value is
782invalid, or returns NODE."
783 (require 'icalendar-parser) ; for ical:printable-value-type-symbol-p
784 (declare-function ical:printable-value-type-symbol-p "icalendar-parser")
785 (let* ((type (ical:ast-node-type node))
786 (value (ical:ast-node-value node))
787 (valtype-param (when (ical:property-type-symbol-p type)
788 (ical:with-param-of node 'ical:valuetypeparam)))
789 (allowed-types
790 (cond ((ical:printable-value-type-symbol-p valtype-param)
791 ;; with an explicit `VALUE=sometype' param, this is the
792 ;; only allowed type:
793 (list valtype-param))
794 ((and (ical:param-type-symbol-p type)
795 (get type 'ical:value-type))
796 (list (get type 'ical:value-type)))
797 ((ical:property-type-symbol-p type)
798 (cons (get type 'ical:default-type)
799 (get type 'ical:other-types)))
800 (t nil))))
801 (cond ((ical:value-type-symbol-p type)
802 (unless (cl-typep value type) ; see `ical:define-type'
803 (ical:signal-validation-error
804 (format "Invalid value for `%s' node: %s" type value)
805 :node node))
806 node)
807 ((ical:component-node-p node)
808 ;; component types have no value, so no need to check anything
809 node)
810 ((and (or (ical:param-type-symbol-p type)
811 (ical:property-type-symbol-p type))
812 (null (get type 'ical:value-type))
813 (stringp value))
814 ;; property and param nodes with no value type are assumed to contain
815 ;; strings which match a value regex:
816 (unless (string-match (rx-to-string (get type 'ical:value-rx)) value)
817 (ical:signal-validation-error
818 (format "Invalid string value for `%s' node: %s" type value)
819 :node node))
820 node)
821 ;; otherwise this is a param or property node which itself
822 ;; should have one or more syntax nodes as a value, so
823 ;; recurse on value(s):
824 ((ical:expects-list-of-values-p type)
825 (unless (listp value)
826 (ical:signal-validation-error
827 (format "Expected list of values for `%s' node" type)
828 :node node))
829 (when allowed-types
830 (dolist (v value)
831 (unless (memq (ical:ast-node-type v) allowed-types)
832 (ical:signal-validation-error
833 (format "Value of unexpected type `%s' in `%s' node"
834 (ical:ast-node-type v) type)
835 :node node))))
836 (mapc #'ical:ast-node-valid-value-p value)
837 node)
838 (t
839 (unless (ical:ast-node-p value)
840 (ical:signal-validation-error
841 (format "Invalid value for `%s' node: %s" type value)
842 :node node))
843 (when allowed-types
844 (unless (memq (ical:ast-node-type value) allowed-types)
845 (ical:signal-validation-error
846 (format "Value of unexpected type `%s' in `%s' node"
847 (ical:ast-node-type value) type)
848 :node node)))
849 (ical:ast-node-valid-value-p value)))))
850
851(defun ical:count-children-by-type (node)
852 "Count NODE's children by type.
853Returns an alist mapping type symbols to the number of NODE's children
854of that type."
855 (let ((children (ical:ast-node-children node))
856 (map nil))
857 (dolist (child children map)
858 (let* ((type (ical:ast-node-type child))
859 (n (alist-get type map)))
860 (setf (alist-get type map) (1+ (or n 0)))))))
861
862(defun ical:ast-node-valid-children-p (node)
863 "Validate that NODE's children satisfy its type's :child-spec.
864
865The :child-spec is associated with NODE's type by
866`icalendar-define-component', `icalendar-define-property',
867`icalendar-define-param', or `icalendar-define-type', which see.
868Signals an `icalendar-validation-error' if NODE is invalid, or returns
869NODE.
870
871Note that this function does not check that the children of NODE
872are themselves valid; for that, see `ical:ast-node-valid-p'."
873 (let* ((type (ical:ast-node-type node))
874 (child-spec (get type 'ical:child-spec))
875 (child-counts (ical:count-children-by-type node)))
876
877 (when child-spec
878
879 (dolist (child-type (plist-get child-spec :one))
880 (unless (= 1 (alist-get child-type child-counts 0))
881 (ical:signal-validation-error
882 (format "iCalendar `%s' node must contain exactly one `%s'"
883 type child-type)
884 :node node)))
885
886 (dolist (child-type (plist-get child-spec :one-or-more))
887 (unless (<= 1 (alist-get child-type child-counts 0))
888 (ical:signal-validation-error
889 (format "iCalendar `%s' node must contain one or more `%s'"
890 type child-type)
891 :node node)))
892
893 (dolist (child-type (plist-get child-spec :zero-or-one))
894 (unless (<= (alist-get child-type child-counts 0)
895 1)
896 (ical:signal-validation-error
897 (format "iCalendar `%s' node may contain at most one `%s'"
898 type child-type)
899 :node node)))
900
901 ;; check that all child nodes are allowed:
902 (unless (plist-get child-spec :allow-others)
903 (let ((allowed-types (append (plist-get child-spec :one)
904 (plist-get child-spec :one-or-more)
905 (plist-get child-spec :zero-or-one)
906 (plist-get child-spec :zero-or-more)))
907 (appearing-types (mapcar #'car child-counts)))
908
909 (dolist (child-type appearing-types)
910 (unless (member child-type allowed-types)
911 (ical:signal-validation-error
912 (format "`%s' may not contain `%s'" type child-type)
913 :node node))))))
914 ;; success:
915 node))
916
917(defun ical:ast-node-valid-p (node &optional recursively)
918 "Check that NODE is a valid iCalendar syntax node.
919By default, the check will only validate NODE itself, but if
920RECURSIVELY is non-nil, it will recursively check all its
921descendants as well. Signals an `icalendar-validation-error' if
922NODE is invalid, or returns NODE."
923 (unless (ical:ast-node-p node)
924 (ical:signal-validation-error
925 "Not an iCalendar syntax node"
926 :node node))
927
928 (ical:ast-node-valid-value-p node)
929 (ical:ast-node-valid-children-p node)
930
931 (let* ((type (ical:ast-node-type node))
932 (other-validator (get type 'ical:other-validator)))
933
934 (unless (ical:type-symbol-p type)
935 (ical:signal-validation-error
936 (format "Node's type `%s' is not an iCalendar type symbol" type)
937 :node node))
938
939 (when (and other-validator (not (functionp other-validator)))
940 (ical:signal-validation-error
941 (format "Bad validator function `%s' for type `%s'" other-validator type)))
942
943 (when other-validator
944 (funcall other-validator node)))
945
946 (when recursively
947 (dolist (c (ical:ast-node-children node))
948 (ical:ast-node-valid-p c recursively)))
949
950 ;; success:
951 node)
952
953(provide 'icalendar-ast)
954;; Local Variables:
955;; read-symbol-shorthands: (("ical:" . "icalendar-"))
956;; End:
957;;; 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..033fea94527
--- /dev/null
+++ b/lisp/calendar/icalendar-macs.el
@@ -0,0 +1,1134 @@
1;;; icalendar-macs.el --- Macros for iCalendar -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Free Software Foundation, Inc.
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: October 2024
7;; Keywords: calendar
8;; Human-Keywords: calendar, iCalendar
9
10;; This file is part of GNU Emacs.
11
12;; This file is free software: you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; This file is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with this file. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; This file defines the macros `ical:define-type', `ical:define-param',
28;; `ical:define-property' and `ical:define-component', used in
29;; icalendar-parser.el to define the particular value types, parameters,
30;; properties and components in the standard as type symbols.
31
32;; TODOs:
33;; - in the define* macros, :default needs rethinking.
34;; I had made this a string because otherwise you can't distinguish
35;; an unspecified default from an explicit "FALSE" for icalendar-boolean
36;; But this might not be true/might not matter anyway, and it's a pain
37;; to have to read the default value where you need it. Probably
38;; should just change these to be the value as read.
39
40
41;;; Code:
42
43(eval-when-compile (require 'cl-lib))
44
45(declare-function ical:ast-node-p "icalendar-ast")
46(declare-function ical:ast-node-type "icalendar-ast")
47(declare-function ical:ast-node-value "icalendar-ast")
48(declare-function ical:type-symbol-p "icalendar-ast")
49(declare-function ical:value-type-symbol-p "icalendar-ast")
50(declare-function ical:expects-list-of-values-p "icalendar-ast")
51
52;; Some utilities:
53
54(defun ical:format-child-spec (child-spec)
55 "Format CHILD-SPEC as a table for use in symbol documentation."
56 (concat
57 (format "%-30s%6s\n" "Type" "Number")
58 (make-string 36 ?-) "\n"
59 (mapconcat
60 (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "1"))
61 (plist-get child-spec :one))
62 (mapconcat
63 (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "1+"))
64 (plist-get child-spec :one-or-more))
65 (mapconcat
66 (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "0-1"))
67 (plist-get child-spec :zero-or-one))
68 (mapconcat
69 (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "0+"))
70 (plist-get child-spec :zero-or-more))))
71
72
73;; Define value types:
74(cl-defmacro ical:define-type (symbolic-name print-name doc specifier matcher
75 &key link
76 (reader #'identity)
77 (printer #'identity))
78 "Define an iCalendar value type named SYMBOLIC-NAME.
79
80PRINT-NAME should be the string used to represent this type in
81the value of an `icalendar-valuetypeparam' property parameter, or
82nil if this is not a type that should be specified there. DOC
83should be a documentation string for the type. SPECIFIER should
84be a type specifier in the sense of `cl-deftype'. MATCHER should
85be an RX definition body (see `rx-define'; argument lists are not
86supported).
87
88Before the type is defined with `cl-deftype', a function will be
89defined named `icalendar-match-PRINT-NAME-value'
90\(or `icalendar-match-OTHER-value', if PRINT-NAME is nil, where
91OTHER is derived from SYMBOLIC-NAME by removing any prefix
92\"icalendar-\" and suffix \"value\"). This function takes a
93string argument and matches it against MATCHER. This function may
94thus occur in SPECIFIER (e.g. in a (satisfies ...) clause).
95
96See the functions `icalendar-read-value-node',
97`icalendar-parse-value-node', and `icalendar-print-value-node' to
98convert values defined with this macro to and from their text
99representation in iCalendar format.
100
101The following keyword arguments are accepted:
102
103:reader - a function to read data of this type. It will be passed
104 a string matching MATCHER and should return an Elisp data structure.
105 Its name does not need to be quoted. (default: identity)
106
107:printer - a function to convert an Elisp data structure of this
108 type to a string. Its name does not need to be quoted.
109 (default: identity)
110
111:link - a string containing a URL for further documentation of this type"
112 (declare (doc-string 2))
113 (let* (;; Related functions:
114 (type-dname (if print-name
115 (downcase print-name)
116 (string-trim
117 (symbol-name symbolic-name)
118 "icalendar-" "value")))
119 (matcher-name (intern (concat "icalendar-match-" type-dname "-value")))
120 ;; Documentation:
121 (header "It names a value type defined by `icalendar-define-type'.")
122 (matcher-doc (format
123"Strings representing values of this type can be matched with
124`%s'.\n" matcher-name))
125 (reader-doc (format "They can be read with `%s'\n" reader))
126 (printer-doc (format "and printed with `%s'." printer))
127 (full-doc (concat header "\n\n" doc "\n\n"
128 matcher-doc reader-doc printer-doc "\n\n"
129"A syntax node of this type can be read with
130`icalendar-read-value-node' or parsed with `icalendar-parse-value-node',
131and printed with `icalendar-print-value-node'.")))
132
133 `(progn
134 ;; Type metadata needs to be available at both compile time and
135 ;; run time. In particular, `ical:value-type-symbol-p' needs to
136 ;; work at compile time.
137 (eval-and-compile
138 (setplist (quote ,symbolic-name)
139 (list
140 'ical:is-type t
141 'ical:is-value t
142 'ical:matcher (function ,matcher-name)
143 'ical:value-rx (quote ,symbolic-name)
144 'ical:value-reader (function ,reader)
145 'ical:value-printer (function ,printer)
146 'ical:type-documentation ,full-doc
147 'ical:link ,link)))
148
149 (rx-define ,symbolic-name
150 ,matcher)
151
152 (defun ,matcher-name (s)
153 ,(format "Match string S against rx `%s'." symbolic-name)
154 (string-match (rx ,symbolic-name) s))
155
156 (cl-deftype ,symbolic-name () ,specifier)
157
158 ;; Store the association between the print name and the type
159 ;; symbol in ical:value-types. The check against print name
160 ;; here allows us to also define value types that aren't
161 ;; "really" types according to the standard, like
162 ;; `ical:geo-coordinates'. Only types that have a
163 ;; print-name can be specified in a VALUE parameter.
164 (when ,print-name
165 (push (cons ,print-name (quote ,symbolic-name)) ical:value-types)))))
166
167;; TODO: not sure this is needed. I've only used it once in the parser.
168(cl-defmacro ical:define-keyword-type (symbolic-name print-name doc matcher
169 &key link
170 (reader 'intern)
171 (printer 'symbol-name))
172 "Like `icalendar-define-type', for types represented by strings.
173String values matching MATCHER are assumed to be type-specific keywords
174that should be interned as symbols when read. (Thus no type specifier
175is necessary: it is always just \\='symbol.) Their printed
176representation is their symbol name."
177 `(ical:define-type ,symbolic-name ,print-name ,doc
178 'symbol
179 ,matcher
180 :link ,link
181 :reader ,reader
182 :printer ,printer))
183
184
185;; Define parameters:
186(cl-defmacro ical:define-param (symbolic-name param-name doc value
187 &key quoted
188 list-sep
189 default
190 (unrecognized default)
191 ((:name-face name-face)
192 'ical:parameter-name nondefault-name-face)
193 ((:value-face value-face)
194 'ical:parameter-value nondefault-value-face)
195 ((:warn-face warn-face)
196 'ical:warning nondefault-warn-face)
197 extra-faces
198 link)
199 "Define iCalendar parameter PARAM-NAME under the symbol SYMBOLIC-NAME.
200PARAM-NAME should be the parameter name as it should appear in
201iCalendar data.
202
203VALUE should either be a symbol for a value type defined with
204`icalendar-define-type', or an `rx' regular expression. If it is
205a type symbol, the regex, reader and printer functions associated
206with that type will be used when parsing and serializing values.
207If it is a regular expression, it is assumed that the values of
208this parameter are strings which match that regular expression.
209
210An `rx' regular expression named SYMBOLIC-NAME which matches the
211parameter is defined:
212 Group 1 of this regex matches PARAM-NAME
213 (or any valid parameter name, if PARAM-NAME is nil).
214 Group 2 matches VALUE, which specifies a correct value
215 for this parameter according to RFC5545.
216 Group 3, if matched, contains any parameter value which does
217 *not* match VALUE, and is incorrect according to the standard.
218
219This regex matches the entire string representing this parameter,
220from \";\" to the end of its value. Another regular expression
221named `SYMBOLIC-NAME-value' is also defined to match just the
222value part, after \";PARAM-NAME=\", with groups 2 and 3 as above.
223
224A function to match the complete parameter expression called
225`icalendar-match-PARAM-NAME-param' is defined
226\(or `icalendar-match-OTHER-param-value' if PARAM-NAME is nil,
227where OTHER is derived from SYMBOLIC-NAME by removing any prefix
228`icalendar-' and suffix `param'). This function is used
229to provide syntax highlighting in `icalendar-mode'.
230
231See the functions `icalendar-read-param-value',
232`icalendar-parse-param-value', `icalendar-parse-params' and
233`icalendar-print-param-node' to convert parameters defined with
234this macro to and from their text representation in iCalendar
235format.
236
237The following keyword arguments are accepted:
238
239:default - a (string representing the) default value, if the
240 parameter is not specified on a given property.
241
242:unrecognized - a (string representing the) value which must be
243 substituted for values that are not recognized but syntactically
244 correct according to RFC5545. Unrecognized values must be in match
245 group 5 of the regex determined by VALUE. An unrecognized value will
246 be preserved in the syntax tree metadata and printed instead of this
247 value when the node is printed. Defaults to any value specified for
248 :default.
249
250:quoted - non-nil if values of this parameter must always be surrounded
251 by (double-)quotation marks when printed, according to RFC5545.
252
253:list-sep - if the parameter accepts a list of values, this should be a
254 string which separates the values (typically \",\"). If :list-sep is
255 non-nil, the value string will first be split on the separator, then
256 if :quoted is non-nil, the individual values will be unquoted, then
257 each value will be read according to VALUE and collected into a list
258 when parsing. When printing, the inverse happens: values are quoted
259 if :quoted is non-nil, then joined with :list-sep. Passing this
260 argument marks SYMBOLIC-NAME as a type that accepts a list of values
261 for `icalendar-expects-list-of-values-p'.
262
263:name-face - a face symbol for highlighting the property name
264 (default: `icalendar-parameter-name')
265
266:value-face - a face symbol for highlighting valid property values
267 (default: `icalendar-parameter-value')
268
269:warn-face - a face symbol for highlighting invalid property values
270 (default: `icalendar-warning')
271
272:extra-faces - a list of the form accepted for HIGHLIGHT in
273 `font-lock-keywords'. In particular,
274 ((GROUPNUM FACENAME [OVERRIDE [LAXMATCH]]) ...)
275 can be used to apply different faces to different
276 match subgroups.
277
278:link - a string containing a URL for documentation of this parameter.
279 The URL will be provided in the documentation shown by
280 `describe-symbol' for SYMBOLIC-NAME."
281 (declare (doc-string 2))
282 (let* (;; Related function names:
283 (param-dname (if param-name
284 (downcase param-name)
285 (string-trim (symbol-name symbolic-name)
286 "icalendar-" "param")))
287 (matcher-name (intern (concat "icalendar-match-" param-dname "-param")))
288 (type-predicate-name
289 (intern (concat "icalendar-" param-dname "-param-p")))
290 ;; Value regexes:
291 (qvalue-rx (if quoted `(seq ?\" ,value ?\") value))
292 (values-rx (when list-sep
293 `(seq ,qvalue-rx (zero-or-more ,list-sep ,qvalue-rx))))
294 (full-value-rx-name
295 (intern (concat (symbol-name symbolic-name) "-value")))
296 ;; Faces:
297 (has-faces (or nondefault-name-face nondefault-value-face
298 nondefault-warn-face extra-faces))
299 ;; Documentation:
300 (header "It names a parameter type defined by `icalendar-define-param'.")
301 (val-list (if list-sep (concat "VAL1" list-sep "VAL2" list-sep "...")
302 "VAL"))
303 (s (if list-sep "s" "")) ; to make plurals
304 (val-doc (concat "VAL" s " "
305 "must be " (unless list-sep "a ") (when quoted "quoted ")
306 (if (ical:value-type-symbol-p value)
307 (format "`%s' value%s" (symbol-name value) s)
308 (format "string%s matching rx `%S'" s value))))
309 (syntax-doc (format "Syntax: %s=%s\n%s"
310 (or param-name "(NAME)") val-list val-doc))
311 (full-doc (concat header "\n\n" doc "\n\n" syntax-doc)))
312
313 `(progn
314 ;; Type metadata needs to be available at both compile time and
315 ;; run time. In particular, `ical:value-type-symbol-p' needs to
316 ;; work at compile time.
317 (eval-and-compile
318 (setplist (quote ,symbolic-name)
319 (list
320 'ical:is-type t
321 'ical:is-param t
322 'ical:matcher (function ,matcher-name)
323 'ical:default-value ,default
324 'ical:is-quoted ,quoted
325 'ical:list-sep ,list-sep
326 'ical:substitute-value ,unrecognized
327 'ical:matcher (function ,matcher-name)
328 'ical:value-type
329 (when (ical:value-type-symbol-p (quote ,value))
330 (quote ,value))
331 'ical:value-rx (quote ,value)
332 'ical:values-rx (quote ,values-rx)
333 'ical:full-value-rx (quote ,full-value-rx-name)
334 'ical:type-documentation ,full-doc
335 'ical:link ,link)))
336
337 ;; Regex which matches just the value of the parameter:
338 ;; Group 2: correct values of the parameter, and
339 ;; Group 3: incorrect values up to the next parameter
340 (rx-define ,full-value-rx-name
341 (or (group-n 2 ,(or values-rx qvalue-rx))
342 (group-n 3 ical:param-value)))
343
344 ;; Regex which matches the full parameter:
345 ;; Group 1: the parameter name,
346 ;; Group 2: correct values of the parameter, and
347 ;; Group 3: incorrect values up to the next parameter
348 (rx-define ,symbolic-name
349 (seq ";"
350 ;; if the parameter name has no printed form, the best we
351 ;; can do is match ical:param-name:
352 (group-n 1 ,(or param-name 'ical:param-name))
353 "="
354 ,full-value-rx-name))
355
356 ;; CL-type to represent syntax nodes for this parameter:
357 (defun ,type-predicate-name (node)
358 ,(format "Return non-nil if NODE represents a %s parameter." param-name)
359 (and (ical:ast-node-p node)
360 (eq (ical:ast-node-type node) (quote ,symbolic-name))))
361
362 (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
363
364 ;; Matcher for the full param string, for syntax highlighting:
365 (defun ,matcher-name (limit)
366 ,(concat (format "Matcher for %s parameter.\n" param-name)
367 "(Defined by `icalendar-define-param'.)")
368 (re-search-forward (rx ,symbolic-name) limit t))
369
370 ;; Entry for font-lock-keywords in icalendar-mode:
371 (when ,has-faces
372 ;; Avoid circular load of icalendar-mode.el in
373 ;; icalendar-parser.el (which does not use the *-face
374 ;; keywords), while still allowing external code to add to
375 ;; font-lock-keywords dynamically:
376 (require 'icalendar-mode)
377 (push (quote (,matcher-name
378 (1 (quote ,name-face) t t)
379 (2 (quote ,value-face) t t)
380 (3 (quote ,warn-face) t t)
381 ,@extra-faces))
382 ical:font-lock-keywords))
383
384 ;; Associate the print name with the type symbol for
385 ;; `ical:parse-params' and `ical:print-param':
386 (when ,param-name
387 (push (cons ,param-name (quote ,symbolic-name)) ical:param-types)))))
388
389
390;; Define properties:
391(cl-defmacro ical:define-property (symbolic-name property-name doc value
392 &key default
393 (unrecognized default)
394 (default-type
395 (if (ical:value-type-symbol-p value)
396 value
397 'ical:text))
398 other-types
399 list-sep
400 child-spec
401 other-validator
402 ((:name-face name-face)
403 'ical:property-name nondefault-name-face)
404 ((:value-face value-face)
405 'ical:property-value nondefault-value-face)
406 ((:warn-face warn-face)
407 'ical:warning nondefault-warn-face)
408 extra-faces
409 link)
410 "Define iCalendar property PROPERTY-NAME under SYMBOLIC-NAME.
411PROPERTY-NAME should be the property name as it should appear in
412iCalendar data.
413
414VALUE should either be a symbol for a value type defined with
415`icalendar-define-type', or an `rx' regular expression. If it is
416a type symbol, the regex, reader and printer functions associated
417with that type will be used when parsing and serializing the
418property's value. If it is a regular expression, it is assumed
419that the values are strings of type `icalendar-text' which match
420that regular expression.
421
422An `rx' regular expression named SYMBOLIC-NAME is defined to
423match the property:
424 Group 1 of this regex matches PROPERTY-NAME.
425 Group 2 matches VALUE.
426 Group 3, if matched, contains any property value which does
427 *not* match VALUE, and is incorrect according to the standard.
428 Group 4, if matched, contains the (unparsed) property parameters;
429 its boundaries can be used for parsing these.
430
431This regex matches the entire string representing this property,
432from the beginning of the content line to the end of its value.
433Another regular expression named `SYMBOLIC-NAME-value' is also
434defined to match just the value part, after the separating colon,
435with groups 2 and 3 as above.
436
437A function to match the complete property expression called
438`icalendar-match-PROPERTY-NAME-property' is defined. This
439function is used to provide syntax highlighting in
440`icalendar-mode'.
441
442See the functions `icalendar-read-property-value',
443`icalendar-parse-property-value', `icalendar-parse-property', and
444`icalendar-print-property-node' to convert properties defined
445with this macro to and from their text representation in
446iCalendar format.
447
448The following keyword arguments are accepted:
449
450:default - a (string representing the) default value, if
451 the property is not specified in a given component.
452
453:unrecognized - a (string representing the) value which must be
454 substituted for values that are not recognized but
455 syntactically correct according to RFC5545. Unrecognized values
456 must be in match group 5 of the regex determined by VALUE. An
457 unrecognized value will be preserved in the syntax tree
458 metadata and printed instead of this value when the node is
459 printed. Defaults to any value specified for :default.
460
461:default-type - a type symbol naming the default type of the
462 property's value. If the property's value differs from this
463 type, an `icalendar-valuetypeparam' parameter will be added to
464 the property's syntax node and printed when the node is
465 printed. Default is VALUE if VALUE is a value type symbol,
466 otherwise the type `icalendar-text'.
467
468:other-types - a list of type symbols naming value types other
469 than :default-type. These represent alternative types for the
470 property's value. If parsing the property's value under its
471 default type fails, these types will be tried in turn, and only
472 if the property's value matches none of them will an error be
473 signaled.
474
475:list-sep - if the property accepts a list of values, this should
476 be a string which separates the values (typically \",\"). If
477 :list-sep is non-nil, the value string will first be split on
478 the separator, then each value will be read according to VALUE
479 and collected into a list when parsing. When printing, the
480 inverse happens: values are printed individually and then
481 joined with :list-sep. Passing this argument marks
482 SYMBOLIC-NAME as a type that accepts a list of values for
483 `icalendar-expects-list-of-values-p'.
484
485:child-spec - a plist mapping the following keywords to lists
486of type symbols:
487 :one - parameters that must appear exactly once
488 :one-or-more - parameters that must appear at least once and
489 may appear more than once
490 :zero-or-one - parameters that must appear at most once
491 :zero-or-more - parameters that may appear more than once
492 :allow-others - if non-nil, other parameters besides those listed in
493 the above are allowed to appear. (In this case, a
494 :zero-or-more clause is redundant.)
495
496:other-validator - a function to perform any additional validation of
497 the component, beyond what `icalendar-ast-node-valid-p' checks.
498 This function should accept one argument, a syntax node. It
499 should return non-nil if the node is valid, or signal an
500 `icalendar-validation-error' if it is not. Its name does not
501 need to be quoted.
502
503:name-face - a face symbol for highlighting the property name
504 (default: `icalendar-property-name')
505
506:value-face - a face symbol for highlighting valid property values
507 (default: `icalendar-property-value')
508
509:warn-face - a face symbol for highlighting invalid property values
510 (default: `icalendar-warning')
511
512:extra-faces - a list of the form for HIGHLIGHT in `font-lock-keywords'.
513 In particular, ((GROUPNUM FACENAME [OVERRIDE [LAXMATCH]])...)
514 can be used to apply different faces to different match subgroups.
515
516:link - a string containing a URL for documentation of this property"
517 (declare (doc-string 2))
518 (let* (;; Value RX:
519 (full-value-rx-name
520 (intern (concat (symbol-name symbolic-name) "-property-value")))
521 (values-rx (when list-sep
522 `(seq ,value (zero-or-more ,list-sep ,value))))
523 ;; Related functions:
524 (property-dname (if property-name
525 (downcase property-name)
526 (string-trim (symbol-name symbolic-name)
527 "icalendar-" "-property")))
528 (matcher-name
529 (intern (concat "icalendar-match-" property-dname "-property")))
530 (type-predicate-name
531 (intern (concat "icalendar-" property-dname "-property-p")))
532 ;; Faces:
533 (has-faces (or nondefault-name-face nondefault-value-face
534 nondefault-warn-face extra-faces))
535 ;; Documentation:
536 (header "It names a property type defined by `icalendar-define-property'.")
537 (val-list (if list-sep (concat "VAL1" list-sep "VAL2" list-sep "...")
538 "VAL"))
539 (default-doc (if default (format "The default value is: \"%s\"\n" default)
540 ""))
541 (s (if list-sep "s" "")) ; to make plurals
542 (val-doc (concat "VAL" s " "
543 "must be " (unless list-sep "a ")
544 (format "value%s of one of the following types:\n" s)
545 (string-join
546 (cons
547 (format "`%s' (default)" default-type)
548 (mapcar (lambda (type) (format "`%s'" type))
549 other-types))
550 "\n")
551 default-doc))
552 (name-doc (if property-name "" "NAME must match rx `icalendar-name'"))
553 (syntax-doc (format "Syntax: %s[;PARAM...]:%s\n%s\n%s\n"
554 (or property-name "NAME") val-list name-doc val-doc))
555 (child-doc
556 (concat
557 "The following parameters are required or allowed\n"
558 "as children in syntax nodes of this type:\n\n"
559 (ical:format-child-spec child-spec)
560 (when (plist-get child-spec :allow-others)
561 "\nOther parameters of any type are also allowed.\n")))
562 (full-doc (concat header "\n\n" doc "\n\n" syntax-doc "\n\n" child-doc)))
563
564 `(progn
565 ;; Type metadata needs to be available at both compile time and
566 ;; run time. In particular, `ical:value-type-symbol-p' needs to
567 ;; work at compile time.
568 (eval-and-compile
569 (setplist (quote ,symbolic-name)
570 (list
571 'ical:is-type t
572 'ical:is-property t
573 'ical:matcher (function ,matcher-name)
574 'ical:default-value ,default
575 'ical:default-type (quote ,default-type)
576 'ical:other-types (quote ,other-types)
577 'ical:list-sep ,list-sep
578 'ical:substitute-value ,unrecognized
579 'ical:value-type
580 (when (ical:value-type-symbol-p (quote ,value))
581 (quote ,value))
582 'ical:value-rx (quote ,value)
583 'ical:values-rx (quote ,values-rx)
584 'ical:full-value-rx (quote ,full-value-rx-name)
585 'ical:child-spec (quote ,child-spec)
586 'ical:other-validator (function ,other-validator)
587 'ical:type-documentation ,full-doc
588 'ical:link ,link)))
589
590 ;; Value regex which matches:
591 ;; Group 2: correct values of the property, and
592 ;; Group 3: incorrect values up to end-of-line (for syntax warnings)
593 (rx-define ,full-value-rx-name
594 (or (group-n 2 ,(or values-rx value))
595 (group-n 3 (zero-or-more not-newline))))
596
597 ;; Full property regex which matches:
598 ;; Group 1: the property name,
599 ;; Group 2: correct values of the property, and
600 ;; Group 3: incorrect values up to end-of-line (for syntax warnings)
601 (rx-define ,symbolic-name
602 (seq line-start
603 (group-n 1 ,(or property-name 'ical:name))
604 (group-n 4 (zero-or-more ical:other-param-safe))
605 ":"
606 ,full-value-rx-name
607 line-end))
608
609 ;; Matcher:
610 (defun ,matcher-name (limit)
611 ,(concat (format "Matcher for `%s' property.\n" symbolic-name)
612 "(Defined by icalendar-define-property.)")
613 (re-search-forward (rx ,symbolic-name) limit t))
614
615 ;; CL-type to represent syntax nodes for this property:
616 (defun ,type-predicate-name (node)
617 ,(format "Return non-nil if NODE represents a %s property." property-name)
618 (and (ical:ast-node-p node)
619 (eq (ical:ast-node-type node) (quote ,symbolic-name))))
620
621 (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
622
623 ;; Associate the print name with the type symbol for
624 ;; `icalendar-parse-property', `icalendar-print-property-node', etc.:
625 (when ,property-name
626 (push (cons ,property-name (quote ,symbolic-name)) ical:property-types))
627
628 ;; Generate an entry for font-lock-keywords in icalendar-mode:
629 (when ,has-faces
630 ;; Avoid circular load of icalendar-mode.el in
631 ;; icalendar-parser.el (which does not use the *-face
632 ;; keywords), while still allowing external code to add to
633 ;; font-lock-keywords dynamically:
634 (require 'icalendar-mode)
635 (push (quote (,matcher-name
636 (1 (quote ,name-face) t t)
637 (2 (quote ,value-face) t t)
638 (3 (quote ,warn-face) t t)
639 ,@extra-faces))
640 ical:font-lock-keywords)))))
641
642
643;; Define components:
644(cl-defmacro ical:define-component (symbolic-name component-name doc
645 &key
646 ((:keyword-face keyword-face)
647 'ical:keyword nondefault-keyword-face)
648 ((:name-face name-face)
649 'ical:component-name nondefault-name-face)
650 child-spec
651 other-validator
652 link)
653 "Define iCalendar component COMPONENT-NAME under SYMBOLIC-NAME.
654COMPONENT-NAME should be the name of the component as it should
655appear in iCalendar data.
656
657Regular expressions to match the component boundaries are defined
658named `COMPONENT-NAME-begin' and `COMPONENT-NAME-end' (or
659`OTHER-begin' and `OTHER-end', where `OTHER' is derived from
660SYMBOLIC-NAME by removing any prefix `icalendar-' and suffix
661`-component' if COMPONENT-NAME is nil).
662 Group 1 of these regexes matches the \"BEGIN\" or \"END\"
663 keyword that marks a component boundary.
664 Group 2 matches the component name.
665
666A function to match the component boundaries is defined called
667`icalendar-match-COMPONENT-NAME-component' (or
668`icalendar-match-OTHER-component', with OTHER as above). This
669function is used to provide syntax highlighting in
670`icalendar-mode'.
671
672The following keyword arguments are accepted:
673
674:child-spec - a plist mapping the following keywords to lists
675of type symbols:
676 :one - properties or components that must appear exactly once
677 :one-or-more - properties or components that must appear at least once and
678 may appear more than once
679 :zero-or-one - properties or components that must appear at most once
680 :zero-or-more - properties or components that may appear more than once
681 :allow-others - if non-nil, other children besides those listed in the above
682 are allowed to appear. (In this case, a :zero-or-more
683 clause is redundant.)
684
685:other-validator - a function to perform any additional validation of
686 the component, beyond what `icalendar-ast-node-valid-p' checks.
687 This function should accept one argument, a syntax node. It
688 should return non-nil if the node is valid, or signal an
689 `icalendar-validation-error' if it is not. Its name does not
690 need to be quoted.
691
692:keyword-face - a face symbol for highlighting the BEGIN/END keyword
693 (default: `icalendar-keyword')
694
695:name-face - a face symbol for highlighting the component name
696 (default: `icalendar-component-name')
697
698:link - a string containing a URL for documentation of this component"
699 (declare (doc-string 2))
700 (let* (;; Regexes:
701 (name-rx (or component-name 'ical:name))
702 (component-dname (if component-name
703 (downcase component-name)
704 (string-trim (symbol-name symbolic-name)
705 "icalendar-" "-component")))
706 (begin-rx-name (intern (concat "icalendar-" component-dname "-begin")))
707 (end-rx-name (intern (concat "icalendar-" component-dname "-end")))
708 ;; Related functions:
709 (matcher-name
710 (intern (concat "icalendar-match-" component-dname "-component")))
711 (type-predicate-name
712 (intern (concat "icalendar-" component-dname "-component-p")))
713 ;; Faces:
714 (has-faces (or nondefault-name-face nondefault-keyword-face))
715 ;; Documentation:
716 (header "It names a component type defined by
717`icalendar-define-component'.")
718 (name-doc (if (not component-name)
719 "\nNAME must match rx `icalendar-name'"
720 ""))
721 (syntax-doc (format "Syntax:\nBEGIN:%s\n[contentline ...]\nEND:%1$s%s"
722 (or component-name "NAME")
723 name-doc))
724 (child-doc
725 (concat
726 "The following properties and components are required or "
727 "allowed\nas children in syntax nodes of this type:\n\n"
728 (ical:format-child-spec child-spec)
729 (when (plist-get child-spec :allow-others)
730 "\nOther properties and components of any type are also allowed.\n")))
731 (full-doc (concat header "\n\n" doc "\n\n" syntax-doc "\n\n" child-doc)))
732
733 `(progn
734 ;; Type metadata needs to be available at both compile time and
735 ;; run time. In particular, `ical:value-type-symbol-p' needs to
736 ;; work at compile time.
737 (eval-and-compile
738 (setplist (quote ,symbolic-name)
739 (list
740 'ical:is-type t
741 'ical:is-component t
742 'ical:matcher (function ,matcher-name)
743 'ical:begin-rx (quote ,begin-rx-name)
744 'ical:end-rx (quote ,end-rx-name)
745 'ical:child-spec (quote ,child-spec)
746 'ical:other-validator (function ,other-validator)
747 'ical:type-documentation ,full-doc
748 'ical:link ,link)))
749
750 ;; Regexes which match:
751 ;; Group 1: BEGIN or END, and
752 ;; Group 2: the component name
753 (rx-define ,begin-rx-name
754 (seq line-start
755 (group-n 1 "BEGIN")
756 ":"
757 (group-n 2 ,name-rx)
758 line-end))
759
760 (rx-define ,end-rx-name
761 (seq line-start
762 (group-n 1 "END")
763 ":"
764 (group-n 2 ,name-rx)
765 line-end))
766
767 (defun ,matcher-name (limit)
768 ,(concat (format "Matcher for %s component boundaries.\n"
769 (or component-name "unrecognized"))
770 "(Defined by `icalendar-define-component'.)")
771 (re-search-forward (rx (or ,begin-rx-name ,end-rx-name)) limit t))
772
773 ;; CL-type to represent syntax nodes for this component:
774 (defun ,type-predicate-name (node)
775 ,(format "Return non-nil if NODE represents a %s component."
776 (or component-name "unrecognized"))
777 (and (ical:ast-node-p node)
778 (eq (ical:ast-node-type node) (quote ,symbolic-name))))
779
780 (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
781
782 ;; Generate an entry for font-lock-keywords in icalendar-mode:
783 (when ,has-faces
784 ;; Avoid circular load of icalendar-mode.el in
785 ;; icalendar-parser.el (which does not use the *-face
786 ;; keywords), while still allowing external code to add to
787 ;; font-lock-keywords dynamically:
788 (require 'icalendar-mode)
789 (push (quote (,matcher-name
790 (1 (quote ,keyword-face) t t)
791 (2 (quote ,name-face) t t)))
792 ical:font-lock-keywords))
793
794 ;; Associate the print name with the type symbol for
795 ;; `icalendar-parse-component', `icalendar-print-component' etc.:
796 (when ,component-name
797 (push (cons ,component-name (quote ,symbolic-name))
798 ical:component-types)))))
799
800
801;; Macros for destructuring and binding AST nodes
802
803(defmacro ical:with-node-children (node bindings &rest body)
804 "Execute BODY with BINDINGS to children in NODE.
805NODE should be an iCalendar syntax node representing a component or
806property.
807
808Each binding in BINDINGS should be a list of one of the following forms:
809
810\(TYPE VAR)
811 TYPE should be a type symbol for an iCalendar property or component
812 which can be a child of COMPONENT. The first child node of TYPE, if
813 any, will be bound to VAR in BODY.
814
815\(TYPE KEY1 VAR1 ...)
816 For each KEY present, the corresponding VAR will be bound as follows:
817 :all - a list of all child nodes of TYPE. If this keyword is present,
818 none of the others are allowed.
819 :first - the first child node of TYPE
820 :default - the default value, if any, for TYPE
821 :value-node - the value of the node in :first
822 :value-type - the type of the node in :value-node (if it is a node).
823 :value - the value of the node in :value-node, if it is a node,
824 or :value-node itself, if it is not.
825 If TYPE expects a list of values, you should use the following keywords
826 instead of the previous three:
827 :value-nodes - the values of the node in :first
828 :value-types - a list of the types of the nodes in :value-nodes.
829 :values - a list of the values of the nodes in :value-nodes (if they are
830 nodes), or the :value-nodes themselves (if they are not).
831 It is a compile-time error to use the singular keywords with a TYPE that
832 takes multiple values, or the plural keywords with a TYPE that does not."
833 (declare (debug (form form &rest form))
834 (indent 2))
835 ;; Static checks on the bindings prevent various annoying bugs:
836 (dolist (b bindings)
837 (let ((type (car b))
838 (kwargs (cdr b)))
839 (unless (ical:type-symbol-p type)
840 (error "Not an iCalendar type symbol: %s" type))
841 (when (and (plist-member kwargs :all)
842 (> 2 (length kwargs)))
843 (error ":all may not be combined with other bindings"))
844 (if (ical:expects-list-of-values-p type)
845 (when (or (plist-member kwargs :value-node)
846 (plist-member kwargs :value-type)
847 (plist-member kwargs :value))
848 (error "Type `%s' expects a list of values" type))
849 (when (or (plist-member kwargs :value-nodes)
850 (plist-member kwargs :value-types)
851 (plist-member kwargs :values))
852 (error "Type `%s' does not expect a list of values" type)))))
853
854 (let ((nd (gensym "icalendar-node")))
855 `(let* ((,nd ,node)
856 ,@(mapcan
857 (lambda (tv)
858 (let ((type (car tv))
859 (vars (cdr tv)))
860 (when (and (symbolp (car vars)) (null (cdr vars)))
861 ;; the simple (TYPE VAR) case:
862 (setq vars (list :first (car vars))))
863
864 (let ((first-var (or (plist-get vars :first)
865 (gensym "first")))
866 (default-var (or (plist-get vars :default)
867 (gensym "default")))
868 (vnode-var (or (plist-get vars :value-node)
869 (gensym "value-node")))
870 (vtype-var (or (plist-get vars :value-type)
871 (gensym "value-type")))
872 (vval-var (or (plist-get vars :value)
873 (gensym "value")))
874
875 (vnodes-var (or (plist-get vars :value-nodes)
876 (gensym "value-nodes")))
877 (vtypes-var (or (plist-get vars :value-types)
878 (gensym "value-types")))
879 (vvals-var (or (plist-get vars :values)
880 (gensym "values")))
881
882 (all-var (or (plist-get vars :all)
883 (gensym "all")))
884 ;; The corresponding vars for :all are mostly
885 ;; too complicated to be useful, I think, so
886 ;; not implementing them for now.
887 ;; TODO: but it *would* be helpful to have an
888 ;; :all-values clause especially for RDATE and
889 ;; EXDATE, since they both accept lists, and
890 ;; can also occur multiple times.
891 ;; I've found myself needing to write
892 ;; (mapcar #'ical:ast-node-value
893 ;; (apply #'append
894 ;; (mapcar #'ical:ast-node-value rdate-nodes))
895 ;; a bit too often.
896 )
897 (delq nil
898 (list
899 (when (plist-member vars :all)
900 `(,all-var (ical:ast-node-children-of
901 (quote ,type) ,nd)))
902 (when (not (plist-member vars :all))
903 `(,first-var (ical:ast-node-first-child-of
904 (quote ,type) ,nd)))
905 (when (plist-member vars :default)
906 `(,default-var (get (quote ,type)
907 'ical:default-value)))
908 ;; Single value:
909 (when (or (plist-member vars :value-node)
910 (plist-member vars :value-type)
911 (plist-member vars :value))
912 `(,vnode-var (when (ical:ast-node-p ,first-var)
913 (ical:ast-node-value ,first-var))))
914 (when (plist-member vars :value-type)
915 `(,vtype-var
916 (when ,vnode-var
917 (ical:ast-node-type ,vnode-var))))
918 (when (plist-member vars :value)
919 `(,vval-var
920 (when ,vnode-var
921 (if (ical:ast-node-p ,vnode-var)
922 (ical:ast-node-value ,vnode-var)
923 ,vnode-var))))
924
925 ;; List of values:
926 (when (or (plist-member vars :value-nodes)
927 (plist-member vars :value-types)
928 (plist-member vars :values))
929 `(,vnodes-var
930 (when (ical:ast-node-p ,first-var)
931 (ical:ast-node-value ,first-var))))
932 (when (plist-member vars :value-types)
933 `(,vtypes-var
934 (when ,vnodes-var
935 (mapcar #'ical:ast-node-type ,vnodes-var))))
936 (when (plist-member vars :values)
937 `(,vvals-var
938 (when ,vnodes-var
939 (if (ical:ast-node-p (car ,vnodes-var))
940 (mapcar #'ical:ast-node-value
941 ,vnodes-var)
942 ,vnodes-var)))))))))
943
944 bindings))
945 ,@body)))
946
947(defalias 'ical:with-component #'ical:with-node-children
948 "Execute BODY with properties of NODE bound as in BINDINGS.
949
950NODE should be an iCalendar syntax node representing an iCalendar
951component: `icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal',
952`icalendar-vtimezone', `icalendar-vfreebusy', `icalendar-standard',
953`icalendar-daylight'. It may also be an entire `icalendar-vcalendar'.
954
955Each binding in BINDINGS should be a list of one of the following forms:
956
957(TYPE VAR)
958 TYPE should be a type symbol for an iCalendar property or component
959 which can be a child of COMPONENT. The first child node of TYPE, if
960 any, will be bound to VAR in BODY.
961
962(TYPE KEY1 VAR1 ...)
963 For each KEY present, the corresponding VAR will be bound as follows:
964 :all - a list of all child nodes of TYPE. If this keyword is present,
965 none of the others are allowed.
966 :default - the default value, if any, for TYPE
967 :first - the first child node of TYPE
968 :value-node - the value (which is itself a node) of the node in :first
969 :value-type - the type of the node in :value-node.
970 :value - the value of the node in :value-node.
971 If TYPE expects a list of values, you should use the following keywords
972 instead of the previous three:
973 :value-nodes - the values (which are themselves nodes) of the node in :first
974 :value-types - a list of the types of the nodes in :value-nodes.
975 :values - a list of the values of the node in :value-node.
976 It is a compile-time error to use the singular keywords with a TYPE that
977 takes multiple values, or the plural keywords with a TYPE that does not.")
978
979(defmacro ical:with-node-value (node &optional bindings &rest body)
980 "Execute BODY with bindings in BINDINGS taken from NODE and its children.
981
982NODE should be an iCalendar syntax node representing a property or
983parameter. If NODE is not a syntax node, this form evalutes to nil
984without binding the variables in BINDINGS and without executing BODY.
985
986Within BODY, if NODE's value is itself a syntax node, the symbol
987`value-node' will be bound to the syntax node for NODE's value,
988`value-type' will be bound to `value-node's type, and `value' will be
989bound to `value-node's value.
990
991If NODE's value is a list of syntax nodes, then within BODY,
992`value-nodes' will be bound to those value nodes, `value-types' will be
993bound to a list of their types, and `values' will be bound to their
994values.
995
996If NODE's value is not a syntax node, then `value' is instead bound
997directly to NODE's value, and `value-type' and `value-node' are bound to
998nil.
999
1000If BODY is nil, it is assumed to be the symbol `value'; thus
1001 (icalendar-with-node-value some-node)
1002is equivalent to
1003 (icalendar-with-node-value some-node nil value)
1004
1005BINDINGS are passed on to `icalendar-with-node-children' and will be
1006available in BODY; see its docstring for their form."
1007 (declare (debug (form &optional form &rest form))
1008 (indent 2))
1009 (let ((vn (gensym "icalendar-node"))
1010 (val (gensym "icalendar-value"))
1011 (is-list (gensym "is-list")))
1012 `(let ((,vn ,node))
1013 (when (ical:ast-node-p ,vn)
1014 (let* ((,val (ical:ast-node-value ,vn))
1015 (value-node (when (ical:ast-node-p ,val) ,val))
1016 (value-type (when (ical:ast-node-p value-node)
1017 (ical:ast-node-type value-node)))
1018 (value (if (ical:ast-node-p value-node)
1019 (ical:ast-node-value value-node)
1020 ,val))
1021 (,is-list (ical:expects-list-of-values-p (ical:ast-node-type ,vn)))
1022 (value-nodes (when ,is-list
1023 (seq-filter #'ical:ast-node-p ,val)))
1024 (value-types (when ,is-list
1025 (mapcar #'ical:ast-node-type value-nodes)))
1026 (values (when ,is-list
1027 (mapcar #'ical:ast-node-value value-nodes))))
1028 (ignore value-type ; Silence the byte compiler when
1029 value ; one of these goes unused
1030 value-types
1031 values)
1032 (ical:with-node-children ,vn ,bindings ,@(or body (list 'value))))))))
1033
1034(defalias 'ical:with-property #'ical:with-node-value
1035 "Execute BODY with BINDINGS taken from the value and parameters in NODE.
1036
1037NODE should be an iCalendar syntax node representing a property. If NODE
1038is not a syntax node, this form evalutes to nil without binding the
1039variables in BINDINGS and without executing BODY.
1040
1041Within BODY, if NODE's value is itself a syntax node, the symbol
1042`value-node' will be bound to the syntax node for NODE's value,
1043`value-type' will be bound to `value-node's type, and `value' will be
1044bound to `value-node's value.
1045
1046If NODE's value is a list of syntax nodes, then within BODY,
1047`value-nodes' will be bound to those value nodes, `value-types' will be
1048bound to a list of their types, and `values' will be bound to their
1049values.
1050
1051If NODE's value is not a syntax node, then `value' is bound directly to
1052NODE's value, and `value-type' and `value-node' are bound to nil.
1053
1054BINDINGS are passed on to `icalendar-with-node-children' and will be
1055available in BODY; see its docstring for their form.")
1056
1057(defmacro ical:with-param (parameter &rest body)
1058 "Bind the value in PARAMETER and execute BODY.
1059
1060PARAMETER should be an iCalendar syntax node representing a
1061parameter. If PARAMETER is nil, this form evalutes to nil without
1062executing BODY.
1063
1064Within BODY, if PARAMETER's value is a syntax node, the symbol
1065`value-node' will be bound to that syntax node, `value-type' will be
1066bound to the value node's type, and `value' will be bound to the value
1067node's value.
1068
1069If PARAMETER's value is not a syntax node, then `value' is bound
1070directly to PARAMETER's value, and `value-type' and `value-node' are
1071bound to nil."
1072 (declare (debug (form &rest form))
1073 (indent 1))
1074 `(ical:with-node-value ,parameter nil ,@body))
1075
1076(defmacro ical:with-child-of (node type &optional bindings &rest body)
1077 "Like `icalendar-with-node-value', but for the relevant node's parent.
1078
1079Find the first child node of type TYPE in NODE, bind that
1080child node's value and any of its children in BINDINGS and execute BODY
1081with these bindings. If there is no such node, this form evalutes to
1082nil without executing BODY.
1083
1084Within BODY, the symbols `value-node', `value-type', and `value' will be
1085bound as in `icalendar-with-node-value'.
1086If BODY is nil, it is assumed to be the symbol `value'; thus
1087 (icalendar-with-child-of some-node some-type)
1088is equivalent to
1089 (icalendar-with-child-of some-node some-type nil value)
1090
1091See `icalendar-with-node-children' for the form of BINDINGS."
1092 (declare (debug (form form &optional form &rest form))
1093 (indent 3))
1094 (let ((child (gensym "icalendar-node")))
1095 `(let ((,child (ical:ast-node-first-child-of ,type ,node)))
1096 (ical:with-node-value ,child ,bindings ,@body))))
1097
1098(defalias 'ical:with-property-of #'ical:with-child-of
1099 "Like `icalendar-with-property', but for components containing that property.
1100
1101Find the first property node of type TYPE in NODE and execute BODY.
1102
1103Within BODY, the symbols `value-node', `value-type', and `value' will be
1104bound to the property's value node, type and value as in
1105`icalendar-with-node-value'. If BODY is nil, it is assumed to be the
1106symbol `value'; thus
1107 (icalendar-with-property-of some-component some-type)
1108is equivalent to
1109 (icalendar-with-property-of some-component some-type nil value)
1110
1111BINDINGS can be used to bind the property's parameters; see
1112`icalendar-with-node-children' for the form of BINDINGS.")
1113
1114(defmacro ical:with-param-of (node type &rest body)
1115 "Like `icalendar-with-param', but for properties containing that param.
1116
1117Find the first parameter node of TYPE in NODE and execute BODY.
1118
1119Within BODY, the symbols `value-node', `value-type', and `value' will be
1120bound to the parameter's value node, type and value as in
1121`icalendar-with-node-value'. If BODY is nil, it is assumed to be the
1122symbol `value'; thus
1123 (icalendar-with-param-of some-property some-type)
1124is equivalent to
1125 (icalendar-with-param-of some-property some-type nil value)"
1126 (declare (debug (form form &rest form))
1127 (indent 2))
1128 `(ical:with-child-of ,node ,type nil ,@body))
1129
1130(provide 'icalendar-macs)
1131;; Local Variables:
1132;; read-symbol-shorthands: (("ical:" . "icalendar-"))
1133;; End:
1134;;; icalendar-macs.el ends here
diff --git a/lisp/calendar/icalendar-mode.el b/lisp/calendar/icalendar-mode.el
new file mode 100644
index 00000000000..c68a912d296
--- /dev/null
+++ b/lisp/calendar/icalendar-mode.el
@@ -0,0 +1,611 @@
1;;; icalendar-mode.el --- Major mode for iCalendar format -*- lexical-binding: t; -*-
2;;;
3
4;; Copyright (C) 2024 Free Software Foundation, Inc.
5
6;; Author: Richard Lawrence <rwl@recursewithless.net>
7;; Created: October 2024
8;; Keywords: calendar
9;; Human-Keywords: calendar, iCalendar
10
11;; This file is part of GNU Emacs.
12
13;; This file is free software: you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
15;; the Free Software Foundation, either version 3 of the License, or
16;; (at your option) any later version.
17
18;; This file is distributed in the hope that it will be useful,
19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;; GNU General Public License for more details.
22
23;; You should have received a copy of the GNU General Public License
24;; along with this file. If not, see <https://www.gnu.org/licenses/>.
25
26;;; Commentary:
27
28;; This file defines icalendar-mode, a major mode for iCalendar data.
29;; Its main job is to provide syntax highlighting using the matching
30;; functions created for iCalendar syntax in icalendar-parser.el, and to
31;; perform line unfolding and folding via format conversion.
32
33;; When activated, icalendar-mode unfolds content lines if necessary.
34;; This is because the parsing functions, and thus syntax highlighting,
35;; assume that content lines have already been unfolded. When a buffer
36;; is saved, icalendar-mode also automatically folds long content if
37;; necessary, as required by RFC5545.
38
39
40;;; Code:
41(require 'icalendar-parser)
42(require 'format)
43
44;; Faces and font lock:
45(defgroup ical:faces
46 '((ical:property-name custom-face)
47 (ical:property-value custom-face)
48 (ical:parameter-name custom-face)
49 (ical:parameter-value custom-face)
50 (ical:component-name custom-face)
51 (ical:keyword custom-face)
52 (ical:binary-data custom-face)
53 (ical:date-time-types custom-face)
54 (ical:numeric-types custom-face)
55 (ical:recurrence-rule custom-face)
56 (ical:warning custom-face)
57 (ical:ignored custom-face))
58 "Faces for `icalendar-mode'."
59 :version "31.1"
60 :group 'icalendar
61 :prefix 'icalendar)
62
63(defface ical:property-name
64 '((default . (:inherit font-lock-keyword-face)))
65 "Face for iCalendar property names.")
66
67(defface ical:property-value
68 '((default . (:inherit default)))
69 "Face for iCalendar property values.")
70
71(defface ical:parameter-name
72 '((default . (:inherit font-lock-property-name-face)))
73 "Face for iCalendar parameter names.")
74
75(defface ical:parameter-value
76 '((default . (:inherit font-lock-property-use-face)))
77 "Face for iCalendar parameter values.")
78
79(defface ical:component-name
80 '((default . (:inherit font-lock-constant-face)))
81 "Face for iCalendar component names.")
82
83(defface ical:keyword
84 '((default . (:inherit font-lock-keyword-face)))
85 "Face for other iCalendar keywords.")
86
87(defface ical:binary-data
88 '((default . (:inherit font-lock-comment-face)))
89 "Face for iCalendar values that represent binary data.")
90
91(defface ical:date-time-types
92 '((default . (:inherit font-lock-type-face)))
93 "Face for iCalendar values that represent time.
94These include dates, date-times, durations, periods, and UTC offsets.")
95
96(defface ical:numeric-types
97 '((default . (:inherit ical:property-value-face)))
98 "Face for iCalendar values that represent integers, floats, and geolocations.")
99
100(defface ical:recurrence-rule
101 '((default . (:inherit font-lock-type-face)))
102 "Face for iCalendar recurrence rule values.")
103
104(defface ical:uri
105 '((default . (:inherit ical:property-value-face :underline t)))
106 "Face for iCalendar values that are URIs (including URLs and mail addresses).")
107
108(defface ical:warning
109 '((default . (:inherit font-lock-warning-face)))
110 "Face for iCalendar syntax errors.")
111
112(defface ical:ignored
113 '((default . (:inherit font-lock-comment-face)))
114 "Face for iCalendar syntax which is parsed but ignored.")
115
116;;; Font lock:
117(defconst ical:params-font-lock-keywords
118 '((ical:match-other-param
119 (1 'font-lock-comment-face t t)
120 (2 'font-lock-comment-face t t)
121 (3 'ical:warning t t))
122 (ical:match-value-param
123 (1 'ical:parameter-name t t)
124 (2 'ical:keyword t t)
125 (3 'ical:warning t t))
126 (ical:match-tzid-param
127 (1 'ical:parameter-name t t)
128 (2 'ical:parameter-value t t)
129 (3 'ical:warning t t))
130 (ical:match-sent-by-param
131 (1 'ical:parameter-name t t)
132 (2 'ical:uri t t)
133 (3 'ical:warning t t))
134 (ical:match-rsvp-param
135 (1 'ical:parameter-name t t)
136 (2 'ical:keyword t t)
137 (3 'ical:warning t t))
138 (ical:match-role-param
139 (1 'ical:parameter-name t t)
140 (2 'ical:keyword t t)
141 (3 'ical:warning t t))
142 (ical:match-reltype-param
143 (1 'ical:parameter-name t t)
144 (2 'ical:keyword t t)
145 (3 'ical:warning t t))
146 (ical:match-related-param
147 (1 'ical:parameter-name t t)
148 (2 'ical:keyword t t)
149 (3 'ical:warning t t))
150 (ical:match-range-param
151 (1 'ical:parameter-name t t)
152 (2 'ical:keyword t t)
153 (3 'ical:warning t t))
154 (ical:match-partstat-param
155 (1 'ical:parameter-name t t)
156 (2 'ical:keyword t t)
157 (3 'ical:warning t t))
158 (ical:match-member-param
159 (1 'ical:parameter-name t t)
160 (2 'ical:uri t t)
161 (3 'ical:warning t t))
162 (ical:match-language-param
163 (1 'ical:parameter-name t t)
164 (2 'ical:parameter-value t t)
165 (3 'ical:warning t t))
166 (ical:match-fbtype-param
167 (1 'ical:parameter-name t t)
168 (2 'ical:keyword t t)
169 (3 'ical:warning t t))
170 (ical:match-fmttype-param
171 (1 'ical:parameter-name t t)
172 (2 'ical:parameter-value t t)
173 (3 'ical:warning t t))
174 (ical:match-encoding-param
175 (1 'ical:parameter-name t t)
176 (2 'ical:keyword t t)
177 (3 'ical:warning t t))
178 (ical:match-dir-param
179 (1 'ical:parameter-name t t)
180 (2 'ical:uri t t)
181 (3 'ical:warning t t))
182 (ical:match-delegated-to-param
183 (1 'ical:parameter-name t t)
184 (2 'ical:uri t t)
185 (3 'ical:warning t t))
186 (ical:match-delegated-from-param
187 (1 'ical:parameter-name t t)
188 (2 'ical:uri t t)
189 (3 'ical:warning t t))
190 (ical:match-cutype-param
191 (1 'ical:parameter-name t t)
192 (2 'ical:keyword t t)
193 (3 'ical:warning t t))
194 (ical:match-cn-param
195 (1 'ical:parameter-name t t)
196 (2 'ical:parameter-value t t)
197 (3 'ical:warning t t))
198 (ical:match-altrep-param
199 (1 'ical:parameter-name t t)
200 (2 'ical:uri t t)
201 (3 'ical:warning t t)))
202 "Entries for iCalendar property parameters in `font-lock-keywords'.")
203
204(defconst ical:properties-font-lock-keywords
205 '((ical:match-request-status-property
206 (1 'ical:property-name t t)
207 (2 'ical:property-value t t)
208 (3 'ical:warning t t))
209 (ical:match-other-property
210 (1 'ical:property-name t t)
211 (2 'ical:property-value t t)
212 (3 'ical:warning t t))
213 (ical:match-sequence-property
214 (1 'ical:property-name t t)
215 (2 'ical:numeric-types t t)
216 (3 'ical:warning t t))
217 (ical:match-last-modified-property
218 (1 'ical:property-name t t)
219 (2 'ical:date-time-types t t)
220 (3 'ical:warning t t))
221 (ical:match-dtstamp-property
222 (1 'ical:property-name t t)
223 (2 'ical:date-time-types t t)
224 (3 'ical:warning t t))
225 (ical:match-created-property
226 (1 'ical:property-name t t)
227 (2 'ical:date-time-types t t)
228 (3 'ical:warning t t))
229 (ical:match-trigger-property
230 (1 'ical:property-name t t)
231 (2 'ical:date-time-types t t)
232 (3 'ical:warning t t))
233 (ical:match-repeat-property
234 (1 'ical:property-name t t)
235 (2 'ical:numeric-types t t)
236 (3 'ical:warning t t))
237 (ical:match-action-property
238 (1 'ical:property-name t t)
239 (2 'ical:keyword t t)
240 (3 'ical:warning t t))
241 (ical:match-rrule-property
242 (1 'ical:property-name t t)
243 (2 'ical:recurrence-rule t t)
244 (3 'ical:warning t t))
245 (ical:match-rdate-property
246 (1 'ical:property-name t t)
247 (2 'ical:date-time-types t t)
248 (3 'ical:warning t t))
249 (ical:match-exdate-property
250 (1 'ical:property-name t t)
251 (2 'ical:date-time-types t t)
252 (3 'ical:warning t t))
253 (ical:match-uid-property
254 (1 'ical:property-name t t)
255 (2 'ical:property-value t t)
256 (3 'ical:warning t t))
257 (ical:match-url-property
258 (1 'ical:property-name t t)
259 (2 'ical:uri t t)
260 (3 'ical:warning t t))
261 (ical:match-related-to-property
262 (1 'ical:property-name t t)
263 (2 'ical:property-value t t)
264 (3 'ical:warning t t))
265 (ical:match-recurrence-id-property
266 (1 'ical:property-name t t)
267 (2 'ical:date-time-types t t)
268 (3 'ical:warning t t))
269 (ical:match-organizer-property
270 (1 'ical:property-name t t)
271 (2 'ical:uri t t)
272 (3 'ical:warning t t))
273 (ical:match-contact-property
274 (1 'ical:property-name t t)
275 (2 'ical:property-value t t)
276 (3 'ical:warning t t))
277 (ical:match-attendee-property
278 (1 'ical:property-name t t)
279 (2 'ical:uri t t)
280 (3 'ical:warning t t))
281 (ical:match-tzurl-property
282 (1 'ical:property-name t t)
283 (2 'ical:uri t t)
284 (3 'ical:warning t t))
285 (ical:match-tzoffsetto-property
286 (1 'ical:property-name t t)
287 (2 'ical:date-time-types t t)
288 (3 'ical:warning t t))
289 (ical:match-tzoffsetfrom-property
290 (1 'ical:property-name t t)
291 (2 'ical:date-time-types t t)
292 (3 'ical:warning t t))
293 (ical:match-tzname-property
294 (1 'ical:property-name t t)
295 (2 'ical:property-value t t)
296 (3 'ical:warning t t))
297 (ical:match-tzid-property
298 (1 'ical:property-name t t)
299 (2 'ical:property-value t t)
300 (3 'ical:warning t t))
301 (ical:match-transp-property
302 (1 'ical:property-name t t)
303 (2 'ical:keyword t t)
304 (3 'ical:warning t t))
305 (ical:match-freebusy-property
306 (1 'ical:property-name t t)
307 (2 'ical:date-time-types t t)
308 (3 'ical:warning t t))
309 (ical:match-duration-property
310 (1 'ical:property-name t t)
311 (2 'ical:date-time-types t t)
312 (3 'ical:warning t t))
313 (ical:match-dtstart-property
314 (1 'ical:property-name t t)
315 (2 'ical:date-time-types t t)
316 (3 'ical:warning t t))
317 (ical:match-due-property
318 (1 'ical:property-name t t)
319 (2 'ical:date-time-types t t)
320 (3 'ical:warning t t))
321 (ical:match-dtend-property
322 (1 'ical:property-name t t)
323 (2 'ical:date-time-types t t)
324 (3 'ical:warning t t))
325 (ical:match-completed-property
326 (1 'ical:property-name t t)
327 (2 'ical:date-time-types t t)
328 (3 'ical:warning t t))
329 (ical:match-summary-property
330 (1 'ical:property-name t t)
331 (2 'ical:property-value t t)
332 (3 'ical:warning t t))
333 (ical:match-status-property
334 (1 'ical:property-name t t)
335 (2 'ical:keyword t t)
336 (3 'ical:warning t t))
337 (ical:match-resources-property
338 (1 'ical:property-name t t)
339 (2 'ical:property-value t t)
340 (3 'ical:warning t t))
341 (ical:match-priority-property
342 (1 'ical:property-name t t)
343 (2 'ical:numeric-types t t)
344 (3 'ical:warning t t))
345 (ical:match-percent-complete-property
346 (1 'ical:property-name t t)
347 (2 'ical:numeric-types t t)
348 (3 'ical:warning t t))
349 (ical:match-location-property
350 (1 'ical:property-name t t)
351 (2 'ical:property-value t t)
352 (3 'ical:warning t t))
353 (ical:match-geo-property
354 (1 'ical:property-name t t)
355 (2 'ical:numeric-types t t)
356 (3 'ical:warning t t))
357 (ical:match-description-property
358 (1 'ical:property-name t t)
359 (2 'ical:property-value t t)
360 (3 'ical:warning t t))
361 (ical:match-comment-property
362 (1 'ical:property-name t t)
363 (2 'ical:property-value t t)
364 (3 'ical:warning t t))
365 (ical:match-class-property
366 (1 'ical:property-name t t)
367 (2 'ical:keyword t t)
368 (3 'ical:warning t t))
369 (ical:match-categories-property
370 (1 'ical:property-name t t)
371 (2 'ical:property-value t t)
372 (3 'ical:warning t t))
373 (ical:match-attach-property
374 (1 'ical:property-name t t)
375 (2 'ical:property-value t t)
376 (3 'ical:warning t t)
377 (13 'ical:uri t t)
378 (14 'ical:binary-data t t))
379 (ical:match-version-property
380 (1 'ical:property-name t t)
381 (2 'ical:property-value t t)
382 (3 'ical:warning t t))
383 (ical:match-prodid-property
384 (1 'ical:property-name t t)
385 (2 'ical:property-value t t)
386 (3 'ical:warning t t))
387 (ical:match-method-property
388 (1 'ical:property-name t t)
389 (2 'ical:property-value t t)
390 (3 'ical:warning t t))
391 (ical:match-calscale-property
392 (1 'ical:property-name t t)
393 (2 'ical:keyword t t)
394 (3 'ical:warning t t)))
395 "Entries for iCalendar properties in `font-lock-keywords'.")
396
397(defconst ical:ignored-properties-font-lock-keywords
398 `((,(rx ical:other-property) (1 'ical:ignored keep t)
399 (2 'ical:ignored keep t)))
400 "Entries for iCalendar ignored properties in `font-lock-keywords'.")
401
402(defconst ical:components-font-lock-keywords
403 '((ical:match-vcalendar-component
404 (1 'ical:keyword t t)
405 (2 'ical:component-name t t))
406 (ical:match-other-component
407 (1 'ical:keyword t t)
408 (2 'ical:component-name t t))
409 (ical:match-valarm-component
410 (1 'ical:keyword t t)
411 (2 'ical:component-name t t))
412 (ical:match-daylight-component
413 (1 'ical:keyword t t)
414 (2 'ical:component-name t t))
415 (ical:match-standard-component
416 (1 'ical:keyword t t)
417 (2 'ical:component-name t t))
418 (ical:match-vtimezone-component
419 (1 'ical:keyword t t)
420 (2 'ical:component-name t t))
421 (ical:match-vfreebusy-component
422 (1 'ical:keyword t t)
423 (2 'ical:component-name t t))
424 (ical:match-vjournal-component
425 (1 'ical:keyword t t)
426 (2 'ical:component-name t t))
427 (ical:match-vtodo-component
428 (1 'ical:keyword t t)
429 (2 'ical:component-name t t))
430 (ical:match-vevent-component
431 (1 'ical:keyword t t)
432 (2 'ical:component-name t t)))
433 "Entries for iCalendar components in `font-lock-keywords'.")
434
435(defvar ical:font-lock-keywords
436 (append ical:params-font-lock-keywords
437 ical:properties-font-lock-keywords
438 ical:components-font-lock-keywords
439 ical:ignored-properties-font-lock-keywords)
440 "Value of `font-lock-keywords' for `icalendar-mode'.")
441
442
443;; The major mode:
444
445;;; Mode hook
446(defvar ical:mode-hook nil
447 "Hook run when activating `icalendar-mode'.")
448
449;;; Activating the mode for .ics files:
450(add-to-list 'auto-mode-alist '("\\.ics\\'" . icalendar-mode))
451
452;;; Syntax table
453(defvar ical:mode-syntax-table
454 (let ((st (make-syntax-table)))
455 ;; Characters for which the standard syntax table suffices:
456 ;; ; (punctuation): separates some property values, and property parameters
457 ;; " (string): begins and ends string values
458 ;; : (punctuation): separates property name (and parameters) from property
459 ;; values
460 ;; , (punctuation): separates values in a list
461 ;; CR, LF (whitespace): content line endings
462 ;; space (whitespace): when at the beginning of a line, continues the
463 ;; previous line
464
465 ;; Characters which need to be adjusted from the standard syntax table:
466 ;; = is punctuation, not a symbol constituent:
467 (modify-syntax-entry ?= ". " st)
468 ;; / is punctuation, not a symbol constituent:
469 (modify-syntax-entry ?/ ". " st)
470 st)
471 "Syntax table used in `icalendar-mode'.")
472
473;;; Coding systems
474
475;; Provide a hint to the decoding system that iCalendar files use DOS
476;; line endings. This appears to be the simplest way to ensure that
477;; `find-file' will correctly decode an iCalendar file, since decoding
478;; happens before icalendar-mode starts.
479(add-to-list 'file-coding-system-alist '("\\.ics\\'" . undecided-dos))
480
481;;; Format conversion
482
483;; We use the format conversion infrastructure provided by format.el,
484;; `insert-file-contents', and `write-region' to automatically perform
485;; line unfolding when icalendar-mode starts in a buffer, and line
486;; folding when it is saved to a file. See Info node `(elisp)Format
487;; Conversion' for more.
488
489(defconst ical:format-definition
490 '(text/calendar "iCalendar format"
491 nil ; no regexp - icalendar-mode runs decode instead
492 ical:unfold-region ; decoding function
493 ical:folding-annotations ; encoding function
494 nil ; encoding function does not modify buffer
495 nil ; no need to activate a minor mode
496 t) ; preserve the format when saving
497 "Entry for iCalendar format in `format-alist'.")
498
499(add-to-list 'format-alist ical:format-definition)
500
501(defun ical:-format-decode-buffer ()
502 "Call `format-decode-buffer' with the \\='text/calendar format.
503This function is intended to be run from `icalendar-mode-hook'."
504 (format-decode-buffer 'text/calendar))
505
506(add-hook 'ical:mode-hook #'ical:-format-decode-buffer -90)
507
508(defun ical:-disable-auto-fill ()
509 "Disable `auto-fill-mode' in iCalendar buffers.
510Auto-fill-mode interferes with line folding and syntax highlighting, so
511it is off by default in iCalendar buffers. This function is intended to
512be run from `icalendar-mode-hook'."
513 (when auto-fill-function
514 (auto-fill-mode -1)))
515
516(add-hook 'ical:mode-hook #'ical:-disable-auto-fill -91)
517
518;;; Commands
519
520(defun ical:switch-to-unfolded-buffer ()
521 "Switch to a new buffer with content lines unfolded.
522The new buffer will contain the same data as the current buffer, but
523with content lines unfolded (before decoding, if possible).
524
525`Folding' means inserting a line break and a single whitespace
526character to continue lines longer than 75 octets; `unfolding'
527means removing the extra whitespace inserted by folding. The
528iCalendar standard (RFC5545) requires folding lines when
529serializing data to iCalendar format, and unfolding before
530parsing it. In `icalendar-mode', folded lines may not have proper
531syntax highlighting; this command allows you to view iCalendar
532data with proper syntax highlighting, as the parser sees it.
533
534If the current buffer is visiting a file, this function will
535offer to save the buffer first, and then reload the contents from
536the file, performing unfolding with `icalendar-unfold-undecoded-region'
537before decoding it. This is the most reliable way to unfold lines.
538
539If it is not visiting a file, it will unfold the new buffer
540with `icalendar-unfold-region'. This can in some cases have
541undesirable effects (see its docstring), so the original contents
542are preserved unchanged in the current buffer.
543
544In both cases, after switching to the new buffer, this command
545offers to kill the original buffer.
546
547It is recommended to turn off `auto-fill-mode' when viewing an
548unfolded buffer, so that filling does not interfere with syntax
549highlighting. This function offers to disable `auto-fill-mode' if
550it is enabled in the new buffer; consider using
551`visual-line-mode' instead."
552 (interactive)
553 (when (and buffer-file-name (buffer-modified-p))
554 (when (y-or-n-p (format "Save before reloading from %s?"
555 (file-name-nondirectory buffer-file-name)))
556 (save-buffer)))
557 (let ((old-buffer (current-buffer))
558 (mmode major-mode)
559 (uf-buffer (if buffer-file-name
560 (ical:unfolded-buffer-from-file buffer-file-name)
561 (ical:unfolded-buffer-from-buffer (current-buffer)))))
562 (switch-to-buffer uf-buffer)
563 ;; restart original major mode, in case the new buffer is
564 ;; still in fundamental-mode: TODO: is this necessary?
565 (funcall mmode)
566 (when (y-or-n-p (format "Unfolded buffer is shown. Kill %s?"
567 (buffer-name old-buffer)))
568 (kill-buffer old-buffer))
569 (when (and auto-fill-function (y-or-n-p "Disable auto-fill-mode?"))
570 (auto-fill-mode -1))))
571
572;;; Mode definition
573;;;###autoload
574(define-derived-mode icalendar-mode text-mode "iCalendar"
575 "Major mode for viewing and editing iCalendar (RFC5545) data.
576
577This mode provides syntax highlighting for iCalendar components,
578properties, values, and property parameters, and defines a format to
579automatically handle folding and unfolding iCalendar content lines.
580
581`Folding' means inserting whitespace characters to continue long
582lines; `unfolding' means removing the extra whitespace inserted
583by folding. The iCalendar standard requires folding lines when
584serializing data to iCalendar format, and unfolding before
585parsing it.
586
587Thus icalendar-mode's syntax highlighting is designed to work with
588unfolded lines. When `icalendar-mode' is activated in a buffer, it will
589automatically unfold lines using a file format conversion, and
590automatically fold lines when saving the buffer to a file; see Info
591node `(elisp)Format Conversion' for more information. It also disables
592`auto-fill-mode' if it is active, since filling interferes with line
593folding and syntax highlighting. Consider using `visual-line-mode' in
594`icalendar-mode' instead."
595 :group 'icalendar
596 :syntax-table ical:mode-syntax-table
597 ;; TODO: Keymap?
598 ;; TODO: buffer-local variables?
599 ;; TODO: indent-line-function and indentation variables
600 ;; TODO: mode-specific menu and context menus
601 ;; TODO: eldoc integration
602 ;; TODO: completion of keywords
603 (progn
604 (setq font-lock-defaults '(ical:font-lock-keywords nil t))))
605
606(provide 'icalendar-mode)
607
608;; Local Variables:
609;; read-symbol-shorthands: (("ical:" . "icalendar-"))
610;; End:
611;;; icalendar-mode.el ends here
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
new file mode 100644
index 00000000000..a2ce4b2362f
--- /dev/null
+++ b/lisp/calendar/icalendar-parser.el
@@ -0,0 +1,4887 @@
1;;; icalendar-parser.el --- Parse iCalendar grammar -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Free Software Foundation, Inc.
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: October 2024
7;; Keywords: calendar
8;; Human-Keywords: calendar, iCalendar
9
10;; This file is part of GNU Emacs.
11
12;; This file is free software: you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; This file is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with this file. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; This file defines regular expressions, constants and functions that
28;; implement the iCalendar grammar according to RFC5545.
29;;
30;; iCalendar data is grouped into *components*, such as events or
31;; to-do items. Each component contains one or more *content lines*,
32;; which each contain a *property* name and its *value*, and possibly
33;; also property *parameters* with additional data that affects the
34;; interpretation of the property.
35;;
36;; The macros `ical:define-type', `ical:define-param',
37;; `ical:define-property' and `ical:define-component', defined in
38;; icalendar-macs.el, each create rx-style regular expressions for one
39;; of these categories in the grammar and are used here to define the
40;; particular value types, parameters, properties and components in the
41;; standard as type symbols. These type symbols store all the metadata
42;; about the relevant types, and are used for type-based dispatch in the
43;; parser and printer functions. In the abstract syntax tree, each node
44;; contains a type symbol naming its type. A number of other regular
45;; expressions which encode basic categories of the grammar are also
46;; defined in this file.
47;;
48;; The following functions provide the high-level interface to the parser:
49;;
50;; `icalendar-parse-and-index'
51;; `icalendar-parse'
52;; `icalendar-parse-calendar'
53;; `icalendar-parse-component'
54;; `icalendar-parse-property'
55;; `icalendar-parse-params'
56;;
57;; The format of the abstract syntax tree which these functions create
58;; is documented in icalendar-ast.el. Nodes in this tree can be
59;; serialized to iCalendar format with the corresponding printer
60;; functions:
61;;
62;; `icalendar-print-calendar-node'
63;; `icalendar-print-component-node'
64;; `icalendar-print-property-node'
65;; `icalendar-print-params'
66
67;;; Code:
68
69(require 'icalendar)
70(eval-when-compile (require 'icalendar-macs))
71(require 'icalendar-ast)
72(eval-when-compile (require 'cl-lib))
73(require 'subr-x)
74(require 'seq)
75(require 'rx)
76(require 'calendar)
77(require 'time-date)
78(require 'simple)
79(require 'help-mode)
80
81;;; Customization
82(defgroup icalendar-parser nil
83 "iCalendar parsing options."
84 :version "31.1"
85 :group 'icalendar
86 :prefix 'icalendar)
87
88(defcustom ical:parse-strictly nil
89 "When non-nil, iCalendar data will be parsed strictly.
90
91By default, the iCalendar parser accepts certain harmless deviations
92from RFC5545 that are common in real-world data (e.g., unescaped commas
93in text values). Setting this to t will cause the parser to produce
94errors instead of silently accepting such data."
95 :version "31.1"
96 :type '(choice (const :tag "Ignore minor errors" nil)
97 (const :tag "Parse strictly" t)))
98
99;;; Functions for folding and unfolding
100;;
101;; According to RFC5545, iCalendar content lines longer than 75 octets
102;; should be *folded* by inserting extra line breaks and leading
103;; whitespace to continue the line. Such lines must be *unfolded*
104;; before they can be parsed. Unfolding can only reliably happen
105;; before Emacs decodes a region of text, because decoding potentially
106;; replaces the CR-LF line endings which terminate content lines.
107;; Programs that can control when decoding happens should use the
108;; stricter `ical:unfold-undecoded-region' to unfold text; programs
109;; that must work with decoded data should use the looser
110;; `ical:unfold-region'. `ical:fold-region' will fold content lines
111;; using line breaks appropriate to the buffer's coding system.
112;;
113;; All the parsing-related code belows assumes that lines have
114;; already been unfolded if necessary.
115(defcustom ical:pre-unfolding-hook nil
116 "Hook run before unfolding iCalendar data.
117
118The functions in this hook will be run before the iCalendar data is
119\"unfolded\", i.e., before whitespace introduced for breaking long lines
120is removed (see `icalendar-unfold-region' and
121`icalendar-unfold-undecoded-region'). If you routinely receive
122iCalendar data that is not correctly folded, you can add functions to
123this hook which clean up that data before unfolding is attempted.
124
125Each function should accept zero arguments and should perform its
126operation on the entire current buffer."
127 :version "31.1"
128 :type '(hook)
129 :options '(ical:fix-line-endings))
130
131(defun ical:fix-line-endings ()
132 "Convert all line endings to LF.
133This function is intended to be used from `icalendar-pre-unfolding-hook'
134(which see) to make files with inconsistent line endings parseable."
135 (when ical:parse-strictly
136 (ical:warn
137 (concat "Converting line endings to LF causes parsing "
138 "errors when `icalendar-parse-strictly' is non-nil.")))
139 (goto-char (point-min))
140 (while (re-search-forward "\r\n?" nil t)
141 (replace-match "\n")))
142
143(defun ical:unfold-undecoded-region (start end &optional buffer)
144 "Unfold an undecoded region in BUFFER between START and END.
145If omitted, BUFFER defaults to the current buffer.
146
147\"Unfolding\" means removing the whitespace characters inserted to
148continue lines longer than 75 octets (see `icalendar-fold-region'
149for the folding operation). RFC5545 specifies these whitespace
150characters to be a CR-LF sequence followed by a single space or
151tab character. Unfolding can only be done reliably before a
152region is decoded, since decoding potentially replaces CR-LF line
153endings.
154
155When `icalendar-parse-strictly' is non-nil, this function searches
156strictly for CR-LF sequences and will fail if they have already been
157replaced, so it should only be called with a region that has not yet
158been decoded. Otherwise, it also searches for folds containing
159Unix-style LF line endings, since these are common in real data."
160 (with-current-buffer (or buffer (current-buffer))
161 (let ((modp (buffer-modified-p)))
162 (with-restriction start end
163 (run-hooks 'ical:pre-unfolding-hook)
164 (goto-char (point-min))
165 ;; Testing reveals that a *significant* amount of real-world data
166 ;; does not use CR-LF line endings, even if it is otherwise
167 ;; OK. So unless we're explicitly parsing strictly, we allow the
168 ;; CR to be missing, as we do in `icalendar-unfold-region':
169 (let ((fold (if ical:parse-strictly (rx (seq "\r\n" (or " " "\t")))
170 (rx (seq (zero-or-one "\r") "\n" (or " " "\t"))))))
171 (while (re-search-forward fold nil t)
172 (replace-match "" nil nil)))
173 ;; merely unfolding should not mark the buffer as modified;
174 ;; this prevents querying the user before killing it:
175 (set-buffer-modified-p modp)))))
176
177(defun ical:unfold-region (start end &optional buffer)
178 "Unfold region between START and END in BUFFER (default: current buffer).
179
180\"Unfolding\" means removing the whitespace characters inserted to
181continue lines longer than 75 octets (see `icalendar-fold-region'
182for the folding operation).
183
184Returns the new end position after unfolding finishes. Thus this
185function is a suitable FROM-FN (decoding function) for `format-alist'.
186
187WARNING: Unfolding can only be done reliably before text is
188decoded, since decoding potentially replaces CR-LF line endings.
189Unfolding an already-decoded region could lead to unexpected
190results, such as displaying multibyte characters incorrectly,
191depending on the contents and the coding system used.
192
193This function attempts to do the right thing even if the region
194is already decoded. If it is still undecoded, it is better to
195call `icalendar-unfold-undecoded-region' directly instead, and
196decode it afterward."
197 ;; TODO: also make this a command so it can be run manually?
198 (with-current-buffer (or buffer (current-buffer))
199 (let ((was-multibyte enable-multibyte-characters)
200 (start-char (position-bytes start))
201 (end-char (position-bytes end))
202 (end-marker (make-marker)))
203 ;; set a marker at the original end position so we can return
204 ;; the updated position later:
205 (set-marker end-marker end)
206 ;; we put the buffer in unibyte mode and later restore its
207 ;; previous state, so that if the buffer was already multibyte,
208 ;; any multibyte characters where line folds broke up their
209 ;; bytes can be reinterpreted:
210 (set-buffer-multibyte nil)
211 (with-restriction start-char end-char
212 (run-hooks 'ical:pre-unfolding-hook)
213 (goto-char (point-min))
214 ;; since we can't be sure that line folds have a leading CR
215 ;; in already-decoded regions, do the best we can:
216 (while (re-search-forward (rx (seq (zero-or-one "\r") "\n"
217 (or " " "\t")))
218 nil t)
219 (replace-match "" nil nil)))
220 ;; restore previous state, possibly reinterpreting characters:
221 (set-buffer-multibyte was-multibyte)
222 ;; return the new end of the region, for format.el conversion:
223 (marker-position end-marker))))
224
225(defun ical:unfolded-buffer-from-region (start end &optional buffer)
226 "Create a new, unfolded buffer with the same contents as the region.
227
228Copies the buffer contents between START and END (in BUFFER, if
229provided) to a new buffer and performs line unfolding in the new buffer
230with `icalendar-unfold-region'. That function can in some cases have
231undesirable effects; see its docstring. If BUFFER is visiting a file, it
232may be better to reload its contents from that file and perform line
233unfolding before decoding; see `icalendar-unfolded-buffer-from-file'.
234Returns the new buffer."
235 (let* ((old-buffer (or buffer (current-buffer)))
236 (contents (with-current-buffer old-buffer
237 (buffer-substring start end)))
238 (uf-buffer (generate-new-buffer ;; TODO: again, move to modeline?
239 (concat " *UNFOLDED:" (buffer-name old-buffer)))))
240 (with-current-buffer uf-buffer
241 (insert contents)
242 (ical:unfold-region (point-min) (point-max))
243 ;; ensure we'll use CR-LF line endings on write, even if they weren't
244 ;; in the source data. The standard also says UTF-8 is the default
245 ;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
246 ;; is nil.
247 (setq buffer-file-coding-system
248 (if last-coding-system-used
249 (coding-system-change-eol-conversion last-coding-system-used
250 'dos)
251 'prefer-utf-8-dos))
252 ;; inhibit auto-save-mode, which will otherwise create save
253 ;; files containing the unfolded data; these are probably
254 ;; not useful to the user and a nuisance when running tests:
255 (auto-save-mode -1))
256 uf-buffer))
257
258(defun ical:unfolded-buffer-from-buffer (buffer)
259 "Create a new, unfolded buffer with the same contents as BUFFER.
260
261Copies the contents of BUFFER to a new buffer and performs line
262unfolding there with `icalendar-unfold-region'. That function can in
263some cases have undesirable effects; see its docstring. If BUFFER is
264visiting a file, it may be better to reload its contents from that file
265and perform line unfolding before decoding; see
266`icalendar-unfolded-buffer-from-file'. Returns the new buffer."
267 (with-current-buffer buffer
268 (ical:unfolded-buffer-from-region (point-min) (point-max) buffer)))
269
270(defun ical:find-unfolded-buffer-visiting (filename)
271 "Find an existing unfolded buffer visiting FILENAME."
272 ;; FIXME: I was previously using
273 ;; (find-buffer-visiting filename #'ical:unfolded-p)
274 ;; for this, but found that it would sometimes return nil even when an
275 ;; unfolded buffer already existed for FILENAME, leading to buffers
276 ;; getting unfolded and parsed multiple times. Hence this kludge.
277 (catch 'unfolded
278 (let ((exp-name (expand-file-name filename)))
279 (dolist (buf (match-buffers "UNFOLDED"))
280 (when (and (equal exp-name (buffer-file-name buf))
281 (ical:unfolded-p buf))
282 (throw 'unfolded buf))))))
283
284(defun ical:unfolded-buffer-from-file (filename &optional visit beg end)
285 "Return a buffer visiting FILENAME with unfolded lines.
286
287If an unfolded buffer is already visiting FILENAME, return
288it. Otherwise, create a new buffer with the contents of FILENAME and
289perform line unfolding with `icalendar-unfold-undecoded-region', then
290decode the buffer, setting an appropriate value for
291`buffer-file-coding-system', and return the new buffer. Optional
292arguments VISIT, BEG, END are as in `insert-file-contents'."
293 (unless (and (file-exists-p filename)
294 (file-readable-p filename))
295 (error "File cannot be read: %s" filename))
296 (or (ical:find-unfolded-buffer-visiting filename)
297 (let ((uf-buffer
298 (generate-new-buffer
299 (concat " *UNFOLDED:" (file-name-nondirectory filename)))))
300 (with-current-buffer uf-buffer
301 (set-buffer-multibyte nil)
302 (insert-file-contents-literally filename visit beg end t)
303 (ical:unfold-undecoded-region (point-min) (point-max))
304 ;; now proceed with decoding:
305 (set-buffer-multibyte t)
306 (decode-coding-inserted-region (point-min) (point-max) filename)
307 ;; ensure we'll use CR-LF line endings on write, even if they weren't
308 ;; in the source data. The standard also says UTF-8 is the default
309 ;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
310 ;; is nil. FIXME: for some reason, this doesn't seem to run at all!
311 (setq buffer-file-coding-system
312 (if last-coding-system-used
313 (coding-system-change-eol-conversion last-coding-system-used
314 'dos)
315 'prefer-utf-8-dos))
316 ;; restore buffer name after renaming by set-visited-file-name:
317 (let ((bname (buffer-name)))
318 (set-visited-file-name filename t)
319 (rename-buffer bname))
320 ;; merely unfolding should not mark the buffer as modified;
321 ;; this prevents querying the user before killing it:
322 (set-buffer-modified-p nil)
323 ;; inhibit auto-save-mode, which will otherwise create save
324 ;; files containing the unfolded data; these are probably
325 ;; not useful to the user and a nuisance when running tests:
326 (auto-save-mode -1))
327 uf-buffer)))
328
329(defun ical:fold-region (begin end &optional annotate-only use-tabs)
330 "Fold content lines between BEGIN and END when longer than 75 octets.
331
332\"Folding\" means inserting a line break and a single space
333character at the beginning of the new line. If USE-TABS is
334non-nil, insert a tab character instead of a single space.
335
336RFC5545 specifies that lines longer than 75 *octets* (excluding
337the line-ending CR-LF sequence) must be folded, and allows that
338some implementations might fold lines in the middle of a
339multibyte character. This function takes care not to do that in a
340buffer where `enable-multibyte-characters' is non-nil, and only
341folds between character boundaries. If the buffer is in unibyte
342mode, however, and contains undecoded multibyte data, it may fold
343lines in the middle of a multibyte character.
344
345By default, this function modifies the region by inserting line folds.
346If the optional argument ANNOTATE-ONLY is non-nil, it will instead leave
347the buffer unmodified, and return a list of \"annotations\"
348\(POSITION . LINE-FOLD), indicating where line folds in the region should
349be inserted. This output is suitable for a function in
350`write-region-annotation-functions'; `icalendar-folding-annotations'
351is a wrapper for this function which can be added to that list."
352 ;; TODO: also make this a command so it can be run manually?
353 (let (annotations)
354 (save-excursion
355 (goto-char begin)
356 (when (not (bolp))
357 (let ((inhibit-field-text-motion t))
358 (beginning-of-line)))
359 (let ((bol (point))
360 (eol (make-marker))
361 (reg-end (make-marker))
362 (line-fold (if use-tabs "\n\t" "\n ")))
363 (set-marker reg-end end)
364 (while (< bol reg-end)
365 (let ((inhibit-field-text-motion t))
366 (end-of-line))
367 (set-marker eol (point))
368 (when (< 75 (- (position-bytes (marker-position eol))
369 (position-bytes bol)))
370 (goto-char
371 ;; the max of 75 excludes the two CR-LF
372 ;; characters we're about to add:
373 (byte-to-position (+ 75 (position-bytes bol))))
374 (if annotate-only
375 (push (cons (point) line-fold) annotations)
376 (insert line-fold))
377 (set-marker eol (point)))
378 (setq bol (goto-char (1+ eol))))))
379 ;; Return annotations, or nil if we modified the buffer directly:
380 (nreverse annotations)))
381
382(defun ical:folding-annotations (start end &optional buffer)
383 "Return a list of annotations for folding lines in the region.
384
385This function is a wrapper for `icalendar-fold-region' that provides the
386interface to be used from `write-region-annotation-functions', which
387see."
388 ;; start may be nil or a string; see `write-region'
389 (if (stringp start)
390 (let ((buf (generate-new-buffer " *icalendar-folded*")))
391 (set-buffer buf)
392 (insert start)
393 (ical:fold-region (point-min) (point-max) t))
394
395 (when (bufferp buffer) (set-buffer buffer))
396 (ical:fold-region (or start (point-min))
397 (if start end (point-max))
398 t)))
399
400(defun ical:contains-folded-lines-p (&optional buffer)
401 "Return non-nil if BUFFER contains folded content lines.
402
403BUFFER defaults to the current buffer. Folded content lines need to be
404unfolded before parsing the buffer or performing syntax
405highlighting. Returns the position at the end of the first fold, or nil."
406 (with-current-buffer (or buffer (current-buffer))
407 (save-excursion
408 (goto-char (point-min))
409 (re-search-forward (rx (seq line-start (or " " "\t")))
410 nil t))))
411
412(defun ical:unfolded-p (&optional buffer)
413 "Return non-nil if BUFFER does not contain any folded content lines.
414BUFFER defaults to the current buffer."
415 (not (ical:contains-folded-lines-p buffer)))
416
417(defun ical:contains-unfolded-lines-p (&optional buffer)
418 "Return non-nil if BUFFER contains long content lines that should be folded.
419
420Lines longer than 75 bytes need to folded before saving or transmitting
421the data in BUFFER (default: current buffer). If BUFFER contains such
422lines, return the position at the beginning of the first line that
423requires folding."
424 (with-current-buffer (or buffer (current-buffer))
425 (save-excursion
426 (goto-char (point-min))
427 (let ((bol (point))
428 (eol (make-marker)))
429 (catch 'unfolded-line
430 (while (< bol (point-max))
431 (let ((inhibit-field-text-motion t))
432 (end-of-line))
433 (set-marker eol (point))
434 ;; the max of 75 excludes the two CR-LF characters
435 ;; after position eol:
436 (when (< 75 (- (position-bytes (marker-position eol))
437 (position-bytes bol)))
438 (throw 'unfolded-line bol))
439 (setq bol (goto-char (1+ eol))))
440 nil)))))
441
442(defun ical:folded-p (&optional buffer)
443 "Return non-nil if BUFFER contains no content lines that require folding.
444BUFFER defaults to the current buffer."
445 (not (ical:contains-unfolded-lines-p buffer)))
446
447
448;; Parsing-related code starts here. All the parsing code assumes that
449;; content lines have already been unfolded.
450
451;;;; Error handling:
452
453;; Errors at the parsing stage:
454;; e.g. value does not match expected regex
455(define-error 'ical:parse-error "Could not parse iCalendar data" 'ical:error)
456
457(cl-defun ical:signal-parse-error (msg &key (buffer (current-buffer))
458 (position (point))
459 (severity 2)
460 (line (line-number-at-pos position))
461 column restart-at)
462 (signal 'ical:parse-error
463 (list :message msg
464 :line line
465 :column column
466 :severity severity
467 :position position
468 :buffer buffer
469 :restart-at restart-at)))
470
471(defun ical:handle-parse-error (err-data &optional skip-msg err-buffer)
472 (let* ((err-sym (car err-data))
473 (err-plist (cdr err-data))
474 (buf (plist-get err-plist :buffer))
475 (restart-pos (plist-get err-plist :restart-at))
476 (new-msg
477 (concat (plist-get err-plist :message)
478 "..."
479 (cond (skip-msg skip-msg)
480 (restart-pos (format "skipping to %d" restart-pos))
481 (t "skipping")))))
482 (setq err-plist (plist-put err-plist :message new-msg))
483 (setq err-plist (plist-put err-plist :severity 1))
484 (ical:handle-generic-error (cons err-sym err-plist) err-buffer)
485 (when restart-pos
486 (with-current-buffer buf
487 (goto-char restart-pos)))))
488
489;; Errors at the printing stage:
490;; e.g. default print function doesn't know how to print value
491(define-error 'ical:print-error "Unable to print iCalendar data" 'ical:error)
492
493(cl-defun ical:signal-print-error (msg &key (severity 2) node)
494 (signal 'ical:print-error
495 (list :message msg
496 :node node
497 :buffer (ical:ast-node-meta-get :buffer node)
498 :severity severity
499 :position (ical:ast-node-meta-get :begin node))))
500
501(defun ical:handle-print-error (err-data &optional skip-msg err-buffer)
502 (let* ((err-sym (car err-data))
503 (err-plist (cdr err-data))
504 (new-msg (concat (plist-get err-plist :message)
505 "..."
506 (or skip-msg "skipping"))))
507 (setq err-plist (plist-put err-plist :message new-msg))
508 (setq err-plist (plist-put err-plist :severity 1))
509 (ical:handle-generic-error (cons err-sym err-plist) err-buffer))
510 (ical:handle-generic-error err-data err-buffer))
511
512;;;; Some utilities:
513(defun ical:parse-from-string (type s)
514 "Parse string S to an iCalendar syntax node of type TYPE.
515S should not contain folded content lines."
516 ;; TODO: support unfolding?
517 (with-temp-buffer
518 (insert s)
519 (goto-char (point-min))
520 (cond ((ical:component-type-symbol-p type)
521 (ical:parse-component (point-max)))
522 ((ical:property-type-symbol-p type)
523 (ical:parse-property (point-max)))
524 ((ical:param-type-symbol-p type)
525 (unless (looking-at-p ";")
526 (insert ";")
527 (backward-char))
528 (ical:parse-params (point-max)))
529 ((ical:value-type-symbol-p type)
530 (ical:parse-value-node type (point-max)))
531 (t
532 (error "Don't know how to parse type %s" type)))))
533
534(defun ical:parse-one-of (types limit)
535 "Parse a value, from point up to LIMIT, of one of the TYPES.
536
537TYPES should be a list of type symbols. For each type in TYPES, the
538parser function associated with that type will be called at point. The
539return value of the first successful parser function is returned. If
540none of the parser functions are able to parse a value, an
541`icalendar-parse-error' is signaled."
542 (let* ((value nil)
543 (start (point))
544 (type (car types))
545 (parser (get type 'ical:value-parser))
546 (rest (cdr types)))
547 (while (and parser (not value))
548 (condition-case nil
549 (setq value (funcall parser limit))
550 (ical:parse-error
551 ;; value of this type not found, so try again:
552 (goto-char start)
553 (setq type (car rest)
554 rest (cdr rest)
555 parser (get type 'ical:value-parser)))))
556 (unless value
557 (ical:signal-parse-error
558 (format "Unable to parse any of %s between %d and %d" types start limit)
559 :position start))
560 value))
561
562(defun ical:read-list-with (reader string
563 &optional value-regex separators omit-nulls trim)
564 "Read a list of values from STRING with READER.
565
566READER should be a reader function that accepts a single string argument.
567SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string'.
568SEPARATORS defaults to \"[^\\][,;]\". TRIM defaults to matching a
569double quote character.
570
571VALUE-REGEX should be a regular expression if READER assumes that
572individual substrings in STRING have previously been matched
573against this regex. In this case, each value in S is placed in a
574temporary buffer and the match against VALUE-REGEX is performed
575before READER is called."
576 (let* ((wrapped-reader
577 (if (not value-regex)
578 ;; no need for temp buffer:
579 reader
580 ;; match the regex in a temp buffer before calling reader:
581 (lambda (s)
582 (with-temp-buffer
583 (insert s)
584 (goto-char (point-min))
585 (unless (looking-at value-regex)
586 (ical:signal-parse-error
587 (format "Expected list of values matching '%s'" value-regex)))
588 (funcall reader (match-string 0))))))
589 (seps (or separators "[^\\][,;]"))
590 (trm (or trim "\""))
591 (raw-values (split-string string seps omit-nulls trm)))
592
593 (unless (functionp reader)
594 (signal 'ical:parser-error
595 (list (format "`%s' is not a reader function" reader))))
596
597 (mapcar wrapped-reader raw-values)))
598
599(defun ical:read-list-of (type string
600 &optional separators omit-nulls trim)
601 "Read a list of values of type TYPE from STRING.
602
603TYPE should be a value type symbol. The reader function
604associated with that type will be called to read the successive
605values in STRING, and the values will be returned as a list of
606syntax nodes.
607
608SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string' and
609will be passed on, if provided, to `icalendar-read-list-with'."
610 (let* ((reader (lambda (s) (ical:read-value-node type s)))
611 (val-regex (rx-to-string (get type 'ical:value-rx))))
612 (ical:read-list-with reader string val-regex
613 separators omit-nulls trim)))
614
615(defun ical:list-of-p (list type)
616 "Return non-nil if each value in LIST satisfies TYPE.
617TYPE should be a type specifier for `cl-typep'."
618 (seq-every-p (lambda (val) (cl-typep val type)) list))
619
620(defun ical:default-value-printer (val)
621 "Default printer for a *single* property or parameter value.
622
623If VAL is a string, just return it unchanged.
624
625Otherwise, VAL should be a syntax node representing a value. In
626that case, return the original string value if another was
627substituted at parse time, or look up the printer function for
628the node's type and call it on the value inside the node.
629
630For properties and parameters that only allow a single value,
631this function should be a sufficient value printer. It is not
632sufficient for those that allow lists of values, or which have
633other special requirements like quoting or escaping."
634 (cond ((stringp val) val)
635 ((and (ical:ast-node-p val)
636 (get (ical:ast-node-type val) 'ical:value-printer))
637 (or (ical:ast-node-meta-get :original-value val)
638 (let* ((stored-value (ical:ast-node-value val))
639 (type (ical:ast-node-type val))
640 (printer (get type 'ical:value-printer)))
641 (funcall printer stored-value))))
642 ;; TODO: other cases to make things easy?
643 ;; e.g. symbols print as their names?
644 (t (ical:signal-print-error
645 (format "Don't know how to print value: %s" val)))))
646
647
648;;; Section 3.1: Content lines
649
650;; Regexp constants for parsing:
651
652;; In the following regexps and define-* declarations, because
653;; Emacs does not have named groups, we observe the following
654;; convention so that the regexps can be combined in sensible ways:
655;;
656;; - Groups 1 through 5 are reserved for the highest-level regexes
657;; created by define-param, define-property and define-component and
658;; used in the match-* functions. Group 1 always represents a 'key'
659;; (e.g. param or property name), group 2 always represents a
660;; correctly parsed value for that key, and group 3 (if matched) an
661;; invalid or unknown value.
662;;
663;; Groups 4 and 5 are reserved for other information in these
664;; highest-level regexes, such as the parameter string between a
665;; property name and its value, or unrecognized values allowed by
666;; the standard and required to be treated like a default value.
667;;
668;; - Groups 6 through 10 are currently unused
669;; - Groups 11 through 20 are reserved for significant sub-expressions
670;; of individual value expressions, e.g. the number of weeks in a
671;; duration value. The various read-* functions rely on these groups
672;; when converting iCalendar data to Elisp data structures.
673
674(rx-define ical:iana-token
675 (one-or-more (any "A-Za-z0-9" "-")))
676
677(rx-define ical:x-name
678 (seq "X-"
679 (zero-or-one (>= 3 (any "A-Za-z0-9")) "-") ; Vendor ID
680 (one-or-more (any "A-Za-z0-9" "-")))) ; Name
681
682(rx-define ical:name
683 (or ical:iana-token ical:x-name))
684
685(rx-define ical:crlf
686 (seq #x12 #xa))
687
688(rx-define ical:control
689 ;; All the controls except HTAB
690 (any (#x00 . #x08) (#x0A . #x1F) #x7F))
691
692;; TODO: double check that "nonascii" class actually corresponds to
693;; the range in the standard
694(rx-define ical:safe-char
695 ;; Any character except ical:control, ?\", ?\;, ?:, ?,
696 (any #x09 #x20 #x21 (#x23 . #x2B) (#x2D . #x39) (#x3C . #x7E) nonascii))
697
698(rx-define ical:qsafe-char
699 ;; Any character except ical:control and ?\"
700 (any #x09 #x20 #x21 (#x23 . #x7E) nonascii))
701
702(rx-define ical:quoted-string
703 (seq ?\" (zero-or-more ical:qsafe-char) ?\"))
704
705(rx-define ical:paramtext
706 ;; RFC5545 allows *zero* characters here, but that would mean we could
707 ;; have parameters like ;FOO=;BAR="somethingelse", and what would then
708 ;; be the value of FOO? I see no reason to allow this and it breaks
709 ;; parameter parsing so I have required at least one char here
710 (one-or-more ical:safe-char))
711
712(rx-define ical:param-name
713 (or ical:iana-token ical:x-name))
714
715(rx-define ical:param-value
716 (or ical:paramtext ical:quoted-string))
717
718(rx-define ical:value-char
719 (any #x09 #x20 (#x21 . #x7E) nonascii))
720
721(rx-define ical:value
722 (zero-or-more ical:value-char))
723
724;; some helpers for brevity, not defined in the standard:
725(rx-define ical:comma-list (item-rx)
726 (seq item-rx
727 (zero-or-more (seq ?, item-rx))))
728
729(rx-define ical:semicolon-list (item-rx)
730 (seq item-rx
731 (zero-or-more (seq ?\; item-rx))))
732
733
734;;; Section 3.3: Property Value Data Types
735
736;; Note: These definitions are here (out of order with respect to the
737;; standard) because a few of them are already required for property
738;; parameter definitions (section 3.2) below.
739
740(defvar ical:value-types nil ;; populated by define-type
741 "Alist mapping value type strings to type symbols.
742Value type strings are those which can appear in `icalendar-valuetypeparam'
743parameters and specify the type of a property's value.")
744
745(defun ical:read-value-node (type s)
746 "Read an iCalendar value of type TYPE from string S to a syntax node.
747Returns a syntax node containing the value."
748 (let ((reader (get type 'ical:value-reader)))
749 (ical:make-ast-node type (list :value (funcall reader s)))))
750
751(defun ical:parse-value-node (type limit)
752 "Parse an iCalendar value of type TYPE from point up to LIMIT.
753Returns a syntax node containing the value."
754 (let ((value-regex (rx-to-string (get type 'ical:value-rx))))
755
756 (unless (re-search-forward value-regex limit t)
757 (ical:signal-parse-error
758 (format "No %s value between %d and %d" type (point) limit)))
759
760 (let ((begin (match-beginning 0))
761 (end (match-end 0))
762 (node (ical:read-value-node type (match-string 0))))
763 (ical:ast-node-meta-set node :buffer (current-buffer))
764 (ical:ast-node-meta-set node :begin begin)
765 (ical:ast-node-meta-set node :end end)
766
767 node)))
768
769(defun ical:print-value-node (node)
770 "Serialize an iCalendar syntax NODE containing a value to a string."
771 (let* ((type (ical:ast-node-type node))
772 (value-printer (get type 'ical:value-printer)))
773 (funcall value-printer (ical:ast-node-value node))))
774
775(defun ical:printable-value-type-symbol-p (symbol)
776 "Return non-nil if SYMBOL represents a printable iCalendar value type.
777
778This means that SYMBOL names a type for a property or parameter value
779defined by `icalendar-define-type' which has a print name (mainly for
780use in `icalendar-valuetypeparam' parameters). That is, SYMBOL must *both*
781satisfy `icalendar-value-type-symbol-p' and be associated with a print
782name in `icalendar-value-types'."
783 (and (ical:value-type-symbol-p symbol)
784 (rassq symbol ical:value-types)))
785
786(defun ical:value-node-p (node)
787 "Return non-nil if NODE is a syntax node whose type is a value type."
788 (and (ical:ast-node-p node)
789 (ical:value-type-symbol-p (ical:ast-node-type node))))
790
791;;;; 3.3.1 Binary
792;; from https://www.rfc-editor.org/rfc/rfc4648#section-4:
793(rx-define ical:base64char
794 (any (?A . ?Z) (?a . ?z) (?0 . ?9) ?+ ?/))
795
796(ical:define-type ical:binary "BINARY"
797 "Type for Binary values.
798
799The parsed and printed representations are the same: a string of characters
800representing base64-encoded data."
801 '(and string (satisfies ical:match-binary-value))
802 (seq (zero-or-more (= 4 ical:base64char))
803 (zero-or-one (or (seq (= 2 ical:base64char) "==")
804 (seq (= 3 ical:base64char) "="))))
805 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.1")
806
807;;;; 3.3.2 Boolean
808(defun ical:read-boolean (s)
809 "Read an `icalendar-boolean' value from a string S.
810S should be a match against rx `icalendar-boolean'."
811 (let ((upcased (upcase s)))
812 (cond ((equal upcased "TRUE") t)
813 ((equal upcased "FALSE") nil)
814 (t (ical:signal-parse-error
815 (format "Expected 'TRUE' or 'FALSE'; got %s" s))))))
816
817(defun ical:print-boolean (b)
818 "Serialize an `icalendar-boolean' value B to a string."
819 (if b "TRUE" "FALSE"))
820
821(ical:define-type ical:boolean "BOOLEAN"
822 "Type for Boolean values.
823
824When printed, either the string 'TRUE' or 'FALSE'.
825When read, either t or nil."
826 'boolean
827 (or "TRUE" "FALSE")
828 :reader ical:read-boolean
829 :printer ical:print-boolean
830 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.2")
831
832;;;; 3.3.3 Calendar User Address
833;; Defined with URI, below
834
835;; Dates and Times:
836
837;;;; 3.3.4 Date
838(cl-deftype ical:numeric-year () '(integer 0 9999))
839(cl-deftype ical:numeric-month () '(integer 1 12))
840(cl-deftype ical:numeric-monthday () '(integer 1 31))
841
842(rx-define ical:year
843 (= 4 digit))
844
845(rx-define ical:month
846 (= 2 digit))
847
848(rx-define ical:mday
849 (= 2 digit))
850
851(defun ical:read-date (s)
852 "Read an `icalendar-date' from a string S.
853S should be a match against rx `icalendar-date'."
854 (let ((year (string-to-number (substring s 0 4)))
855 (month (string-to-number (substring s 4 6)))
856 (day (string-to-number (substring s 6 8))))
857 (list month day year)))
858
859(defun ical:print-date (d)
860 "Serialize an `icalendar-date' to a string."
861 (format "%04d%02d%02d"
862 (calendar-extract-year d)
863 (calendar-extract-month d)
864 (calendar-extract-day d)))
865
866(ical:define-type ical:date "DATE"
867 "Type for Date values.
868
869When printed, a date is a string of digits in YYYYMMDD format.
870
871When read, a date is a list (MONTH DAY YEAR), with the three
872values being integers in the appropriate ranges; see calendar.el
873for functions that work with this representation."
874 '(and (satisfies calendar-date-is-valid-p))
875 (seq ical:year ical:month ical:mday)
876 :reader ical:read-date
877 :printer ical:print-date
878 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.4")
879
880;;;; 3.3.12 Time
881;; (Defined here so that ical:time RX can be used in ical:date-time)
882(cl-deftype ical:numeric-hour () '(integer 0 23))
883(cl-deftype ical:numeric-minute () '(integer 0 59))
884(cl-deftype ical:numeric-second () '(integer 0 60)) ; 60 represents a leap second
885
886(defun ical:read-time (s)
887 "Read an `icalendar-time' from a string S.
888S should be a match against rx `icalendar-time'."
889 (require 'icalendar-utils) ; for ical:make-date-time; avoids circular require
890 (declare-function ical:make-date-time "icalendar-utils")
891 (let ((hour (string-to-number (substring s 0 2)))
892 (minute (string-to-number (substring s 2 4)))
893 (second (string-to-number (substring s 4 6)))
894 (utcoffset (if (and (length= s 7)
895 (equal "Z" (substring s 6 7)))
896 0
897 ;; unknown/'floating' time zone:
898 nil)))
899 (ical:make-date-time :second second
900 :minute minute
901 :hour hour
902 :zone utcoffset)))
903
904(defun ical:print-time (time)
905 "Serialize an `icalendar-time' to a string."
906 (format "%02d%02d%02d%s"
907 (decoded-time-hour time)
908 (decoded-time-minute time)
909 (decoded-time-second time)
910 (if (eql 0 (decoded-time-zone time))
911 "Z" "")))
912
913(defun ical:-decoded-time-p (val)
914 "Return non-nil if VAL is a valid decoded *time*.
915This predicate does not check date-related values in VAL;
916for that, see `icalendar--decoded-date-time-p'."
917 (and (listp val)
918 (length= val 9)
919 (cl-typep (decoded-time-second val) 'ical:numeric-second)
920 (cl-typep (decoded-time-minute val) 'ical:numeric-minute)
921 (cl-typep (decoded-time-hour val) 'ical:numeric-hour)
922 (cl-typep (decoded-time-dst val) '(member t nil -1))
923 (cl-typep (decoded-time-zone val) '(or integer null))))
924
925(ical:define-type ical:time "TIME"
926 "Type for Time values.
927
928When printed, a time is a string of six digits HHMMSS, followed
929by the letter 'Z' if it is in UTC.
930
931When read, a time is a decoded time, i.e. a list in the format
932(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
933`decode-time' for the specifics of the individual values. When
934read, the DAY, MONTH, YEAR, and DOW fields are nil, and these
935fields and DST are ignored when printed."
936 '(satisfies ical:-decoded-time-p)
937 (seq (= 6 digit) (zero-or-one ?Z))
938 :reader ical:read-time
939 :printer ical:print-time
940 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.12")
941
942;;;; 3.3.5 Date-Time
943(defun ical:-decoded-date-time-p (val)
944 (and (listp val)
945 (length= val 9)
946 (cl-typep (decoded-time-second val) 'ical:numeric-second)
947 (cl-typep (decoded-time-minute val) 'ical:numeric-minute)
948 (cl-typep (decoded-time-hour val) 'ical:numeric-hour)
949 (cl-typep (decoded-time-day val) 'ical:numeric-monthday)
950 (cl-typep (decoded-time-month val) 'ical:numeric-month)
951 (cl-typep (decoded-time-year val) 'ical:numeric-year)
952 (calendar-date-is-valid-p (list (decoded-time-month val)
953 (decoded-time-day val)
954 (decoded-time-year val)))
955 ;; FIXME: the weekday slot value should be automatically
956 ;; calculated from month, day, and year, like:
957 ;; (calendar-day-of-week (list month day year))
958 ;; Although `ical:read-date-time' does this correctly,
959 ;; `make-decoded-time' does not. Thus we can't use
960 ;; `make-decoded-time' to construct valid `ical:date-time'
961 ;; values unless this check is turned off,
962 ;; which means it's annoying to write tests and anything
963 ;; that uses cl-typecase to dispatch on values created by
964 ;; `make-decoded-time':
965 ;; (cl-typep (decoded-time-weekday val) '(integer 0 6))
966 (cl-typep (decoded-time-dst val) '(member t nil -1))
967 (cl-typep (decoded-time-zone val) '(or integer null))))
968
969(defun ical:read-date-time (s)
970 "Read an `icalendar-date-time' from a string S.
971S should be a match against rx `icalendar-date-time'."
972 (require 'icalendar-utils) ; for ical:make-date-time; avoids circular requires
973 (let ((year (string-to-number (substring s 0 4)))
974 (month (string-to-number (substring s 4 6)))
975 (day (string-to-number (substring s 6 8)))
976 ;; "T" is index 8
977 (hour (string-to-number (substring s 9 11)))
978 (minute (string-to-number (substring s 11 13)))
979 (second (string-to-number (substring s 13 15)))
980 (utcoffset (if (and (length= s 16)
981 (equal "Z" (substring s 15 16)))
982 0
983 ;; unknown/'floating' time zone:
984 nil)))
985 (ical:make-date-time :second second
986 :minute minute
987 :hour hour
988 :day day
989 :month month
990 :year year
991 :zone utcoffset)))
992
993(defun ical:print-date-time (datetime)
994 "Serialize an `icalendar-date-time' to a string."
995 (format "%04d%02d%02dT%02d%02d%02d%s"
996 (decoded-time-year datetime)
997 (decoded-time-month datetime)
998 (decoded-time-day datetime)
999 (decoded-time-hour datetime)
1000 (decoded-time-minute datetime)
1001 (decoded-time-second datetime)
1002 (if (ical:date-time-is-utc-p datetime)
1003 "Z" "")))
1004
1005(defun ical:date-time-is-utc-p (datetime)
1006 "Return non-nil if DATETIME is in UTC time."
1007 (let ((offset (decoded-time-zone datetime)))
1008 (and offset (= 0 offset))))
1009
1010(ical:define-type ical:date-time "DATE-TIME"
1011 "Type for Date-Time values.
1012
1013When printed, a date-time is a string of digits like:
1014 YYYYMMDDTHHMMSS
1015where the 'T' is literal, and separates the date string from the
1016time string.
1017
1018When read, a date-time is a decoded time, i.e. a list in the format
1019(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
1020`decode-time' for the specifics of the individual values."
1021 '(satisfies ical:-decoded-date-time-p)
1022 (seq ical:date ?T ical:time)
1023 :reader ical:read-date-time
1024 :printer ical:print-date-time
1025 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5")
1026
1027;;;; 3.3.6 Duration
1028(rx-define ical:dur-second
1029 (seq (group-n 19 (one-or-more digit)) ?S))
1030
1031(rx-define ical:dur-minute
1032 (seq (group-n 18 (one-or-more digit)) ?M (zero-or-one ical:dur-second)))
1033
1034(rx-define ical:dur-hour
1035 (seq (group-n 17 (one-or-more digit)) ?H (zero-or-one ical:dur-minute)))
1036
1037(rx-define ical:dur-day
1038 (seq (group-n 16 (one-or-more digit)) ?D))
1039
1040(rx-define ical:dur-week
1041 (seq (group-n 15 (one-or-more digit)) ?W))
1042
1043(rx-define ical:dur-time
1044 (seq ?T (or ical:dur-hour ical:dur-minute ical:dur-second)))
1045
1046(rx-define ical:dur-date
1047 (seq ical:dur-day (zero-or-one ical:dur-time)))
1048
1049(defun ical:read-dur-value (s)
1050 "Read an `icalendar-dur-value' from a string S.
1051S should be a match against rx `icalendar-dur-value'."
1052 ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
1053 (ignore s)
1054 (let ((sign (if (equal (match-string 20) "-") -1 1)))
1055 (if (match-string 15)
1056 ;; dur-value specified in weeks, so just return an integer:
1057 (* sign (string-to-number (match-string 15)))
1058 ;; otherwise, make a time delta from the other units:
1059 (let* ((days (match-string 16))
1060 (ndays (* sign (if days (string-to-number days) 0)))
1061 (hours (match-string 17))
1062 (nhours (* sign (if hours (string-to-number hours) 0)))
1063 (minutes (match-string 18))
1064 (nminutes (* sign (if minutes (string-to-number minutes) 0)))
1065 (seconds (match-string 19))
1066 (nseconds (* sign (if seconds (string-to-number seconds) 0))))
1067 (make-decoded-time :second nseconds :minute nminutes :hour nhours
1068 :day ndays)))))
1069
1070(defun ical:print-dur-value (dur)
1071 "Serialize an `icalendar-dur-value' to a string."
1072 (if (integerp dur)
1073 ;; dur-value specified in weeks can only contain weeks:
1074 (format "%sP%dW" (if (< dur 0) "-" "") (abs dur))
1075 ;; otherwise, show all the time units present:
1076 (let* ((days+- (or (decoded-time-day dur) 0))
1077 (hours+- (or (decoded-time-hour dur) 0))
1078 (minutes+- (or (decoded-time-minute dur) 0))
1079 (seconds+- (or (decoded-time-second dur) 0))
1080 ;; deal with the possibility of mixed positive and negative values
1081 ;; in a time delta list:
1082 (sum (+ seconds+-
1083 (* 60 minutes+-)
1084 (* 60 60 hours+-)
1085 (* 60 60 24 days+-)))
1086 (abssum (abs sum))
1087 (days (/ abssum (* 60 60 24)))
1088 (sumnodays (mod abssum (* 60 60 24)))
1089 (hours (/ sumnodays (* 60 60)))
1090 (sumnohours (mod sumnodays (* 60 60)))
1091 (minutes (/ sumnohours 60))
1092 (seconds (mod sumnohours 60))
1093 (sign (when (< sum 0) "-"))
1094 (time-sep (unless (and (zerop hours) (zerop minutes) (zerop seconds))
1095 "T")))
1096 (concat sign
1097 "P"
1098 (unless (zerop days) (format "%dD" days))
1099 time-sep
1100 (unless (zerop hours) (format "%dH" hours))
1101 (unless (zerop minutes) (format "%dM" minutes))
1102 (unless (zerop seconds) (format "%dS" seconds))))))
1103
1104(defun ical:-time-delta-p (val)
1105 (and (listp val)
1106 (length= val 9)
1107 (let ((seconds (decoded-time-second val))
1108 (minutes (decoded-time-minute val))
1109 (hours (decoded-time-hour val))
1110 (days (decoded-time-day val))) ; other values in list are ignored
1111 (or (and (integerp seconds) (not (zerop seconds)))
1112 (and (integerp minutes) (not (zerop minutes)))
1113 (and (integerp hours) (not (zerop hours)))
1114 (and (integerp days) (not (zerop days)))))))
1115
1116(ical:define-type ical:dur-value "DURATION"
1117 "Type for Duration values.
1118
1119When printed, a duration is a string containing:
1120 - possibly a +/- sign
1121 - the letter 'P'
1122 - one or more sequences of digits followed by a letter representing a unit
1123 of time: 'W' for weeks, 'D' for days, etc. Units smaller than a day are
1124 separated from days by the letter 'T'. If a duration is specified in weeks,
1125 other units of time are not allowed.
1126
1127For example, a duration of 15 days, 5 hours, and 20 seconds would be printed:
1128 P15DT5H0M20S
1129and a duration of 7 weeks would be printed:
1130 P7W
1131
1132When read, a duration is either an integer, in which case it
1133represents a number of weeks, or a decoded time, in which case it
1134must represent a time delta in the sense of `decoded-time-add'.
1135Note that, in the time delta representation, units of time longer
1136than a day are not supported and will be ignored if present.
1137
1138This type is named `icalendar-dur-value' rather than
1139`icalendar-duration' for consistency with the text of RFC5545 and
1140so that its name does not collide with the symbol for the
1141`DURATION' property."
1142 '(or integer (satisfies ical:-time-delta-p))
1143 ;; Group 15: weeks
1144 ;; Group 16: days
1145 ;; Group 17: hours
1146 ;; Group 18: minutes
1147 ;; Group 19: seconds
1148 ;; Group 20: sign
1149 (seq
1150 (group-n 20 (zero-or-one (or ?+ ?-)))
1151 ?P
1152 (or ical:dur-date ical:dur-time ical:dur-week))
1153 :reader ical:read-dur-value
1154 :printer ical:print-dur-value
1155 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6")
1156
1157
1158;;;; 3.3.7 Float
1159(ical:define-type ical:float "FLOAT"
1160 "Type for Float values.
1161
1162When printed, possibly a sign + or -, followed by a sequence of digits,
1163and possibly a decimal. When read, an Elisp float value."
1164 '(float * *)
1165 (seq
1166 (zero-or-one (or ?+ ?-))
1167 (one-or-more digit)
1168 (zero-or-one (seq ?. (one-or-more digit))))
1169 :reader string-to-number
1170 :printer number-to-string
1171 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.7")
1172
1173;;;; 3.3.8 Integer
1174(ical:define-type ical:integer "INTEGER"
1175 "Type for Integer values.
1176
1177When printed, possibly a sign + or -, followed by a sequence of digits.
1178When read, an Elisp integer value between -2147483648 and 2147483647."
1179 '(integer -2147483648 2147483647)
1180 (seq
1181 (zero-or-one (or ?+ ?-))
1182 (one-or-more digit))
1183 :reader string-to-number
1184 :printer number-to-string
1185 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.8")
1186
1187;;;; 3.3.9 Period
1188(defsubst ical:period-start (period)
1189 "Return the `icalendar-date-time' which marks the start of PERIOD."
1190 (car period))
1191
1192(defsubst ical:period--defined-end (period)
1193 "Return the `icalendar-date-time' which marks the end of PERIOD, or nil."
1194 (cadr period))
1195
1196(defsubst ical:period-dur-value (period)
1197 "Return the `icalendar-dur-value' which gives the length of PERIOD, or nil."
1198 (caddr period))
1199
1200(defun ical:period-end (period &optional vtimezone)
1201 "Return the `icalendar-date-time' which marks the end of PERIOD.
1202If the end is not explicitly specified, it will be computed from the
1203period's start and duration. VTIMEZONE, if given, should be the
1204`icalendar-vtimezone' in which to compute the end time."
1205 (require 'icalendar-utils) ; for date/time-add-duration; avoids circular import
1206 (declare-function ical:date/time-add-duration "icalendar-utils")
1207 (or (ical:period--defined-end period)
1208 ;; compute end from duration and cache it:
1209 (setf (cadr period)
1210 (ical:date/time-add-duration
1211 (ical:period-start period)
1212 (ical:period-dur-value period)
1213 vtimezone))))
1214
1215(defun ical:period-p (val)
1216 (and (listp val)
1217 (length= val 3)
1218 (cl-typep (ical:period-start val) 'ical:date-time)
1219 (cl-typep (ical:period-end val) '(or null ical:date-time))
1220 (cl-typep (ical:period-dur-value val) '(or null ical:dur-value))))
1221
1222(cl-defun ical:make-period (start &key end duration)
1223 "Make an `icalendar-period' value.
1224
1225START and END (if given) should be `icalendar-date-time' values.
1226DURATION, if given, should be an `icalendar-dur-value'. It is an error
1227to pass both END and DURATION, or neither."
1228 (when (and end duration)
1229 (signal 'wrong-type-argument (list end duration)))
1230 (unless (or end duration)
1231 (signal 'wrong-type-argument (list end duration)))
1232 (list start end duration))
1233
1234(defun ical:read-period (s)
1235 "Read an `icalendar-period' from a string S.
1236S should have been matched against rx `icalendar-period'."
1237 ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
1238 (ignore s)
1239 (let ((start (ical:read-date-time (match-string 11)))
1240 (end (when (match-string 12) (ical:read-date-time (match-string 12))))
1241 (dur (when (match-string 13) (ical:read-dur-value (match-string 13)))))
1242 (ical:make-period start :end end :duration dur)))
1243
1244(defun ical:print-period (per)
1245 "Serialize an `icalendar-period' to a string."
1246 (let ((start (ical:period-start per))
1247 (end (ical:period-end per))
1248 (dur (ical:period-dur-value per)))
1249 (concat (ical:print-date-time start)
1250 "/"
1251 (if dur
1252 (ical:print-dur-value dur)
1253 (ical:print-date-time end)))))
1254
1255(ical:define-type ical:period "PERIOD"
1256 "Type for Period values.
1257
1258A period of time is specified as a starting date-time together
1259with either an explicit date-time as its end, or a duration which
1260gives its length and implicitly marks its end.
1261
1262When printed, the starting date-time is separated from the end or
1263duration by a / character.
1264
1265When read, a period is represented as a list (START END DUR), where
1266START is an `icalendar-date-time', END is either an
1267`icalendar-date-time' or nil, and DUR is either an `icalendar-dur-value'
1268or nil. See the functions `icalendar-make-period',
1269`icalendar-period-start', `icalendar-period-end', and
1270`icalendar-period-dur-value' to work with period values."
1271 '(satisfies ical:period-p)
1272 (seq (group-n 11 ical:date-time)
1273 "/"
1274 (or (group-n 12 ical:date-time)
1275 (group-n 13 ical:dur-value)))
1276 :reader ical:read-period
1277 :printer ical:print-period
1278 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.9")
1279
1280;;;; 3.3.10 Recurrence rules:
1281(rx-define ical:freq
1282 (or "SECONDLY" "MINUTELY" "HOURLY" "DAILY" "WEEKLY" "MONTHLY" "YEARLY"))
1283
1284(rx-define ical:weekday
1285 (or "SU" "MO" "TU" "WE" "TH" "FR" "SA"))
1286
1287(rx-define ical:ordwk
1288 (** 1 2 digit)) ; 1 to 53
1289
1290(rx-define ical:weekdaynum
1291 ;; Group 19: Week num, if present
1292 ;; Group 20: week day abbreviation
1293 (seq (zero-or-one
1294 (group-n 19 (seq (zero-or-one (or ?+ ?-))
1295 ical:ordwk)))
1296 (group-n 20 ical:weekday)))
1297
1298(rx-define ical:weeknum
1299 (seq (zero-or-one (or ?+ ?-))
1300 ical:ordwk))
1301
1302(rx-define ical:monthdaynum
1303 (seq (zero-or-one (or ?+ ?-))
1304 (** 1 2 digit))) ; 1 to 31
1305
1306(rx-define ical:monthnum
1307 (seq (zero-or-one (or ?+ ?-))
1308 (** 1 2 digit))) ; 1 to 12
1309
1310(rx-define ical:yeardaynum
1311 (seq (zero-or-one (or ?+ ?-))
1312 (** 1 3 digit))) ; 1 to 366
1313
1314(defconst ical:weekday-numbers
1315 '(("SU" . 0)
1316 ("MO" . 1)
1317 ("TU" . 2)
1318 ("WE" . 3)
1319 ("TH" . 4)
1320 ("FR" . 5)
1321 ("SA" . 6))
1322 "Alist mapping two-letter weekday abbreviations to numbers 0 to 6.
1323Weekday abbreviations in recurrence rule parts are translated to
1324and from numbers for compatibility with calendar-* and
1325decoded-time-* functions.")
1326
1327(defun ical:read-weekdaynum (s)
1328 "Read a weekday abbreviation to a number.
1329If the abbreviation is preceded by an offset, read a dotted
1330pair (WEEKDAY . OFFSET). Thus \"SU\" becomes 0, \"-1SU\"
1331becomes (0 . -1), etc. S should have been matched against
1332`icalendar-weekdaynum'."
1333 ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
1334 (ignore s)
1335 (let ((dayno (cdr (assoc (match-string 20) ical:weekday-numbers)))
1336 (weekno (match-string 19)))
1337 (if weekno
1338 (cons dayno (string-to-number weekno))
1339 dayno)))
1340
1341(defun ical:print-weekdaynum (val)
1342 "Serialize a number or dotted pair VAL to a string.
1343The result is in the format required for a BYDAY recurrence rule clause.
1344See `icalendar-read-weekdaynum' for the format of VAL."
1345 (if (consp val)
1346 (let* ((dayno (car val))
1347 (day (car (rassq dayno ical:weekday-numbers)))
1348 (offset (cdr val)))
1349 (concat (number-to-string offset) day))
1350 ;; number alone just stands for a day:
1351 (car (rassq val ical:weekday-numbers))))
1352
1353(defun ical:read-recur-rule-part (s)
1354 "Read an `icalendar-recur-rule-part' from string S.
1355S should have been matched against `icalendar-recur-rule-part'.
1356The return value is a list (KEYWORD VALUE), where VALUE may
1357itself be a list, depending on the values allowed by KEYWORD."
1358 ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
1359 (ignore s)
1360 (let ((keyword (intern (upcase (match-string 11))))
1361 (values (match-string 12)))
1362 (list keyword
1363 (cl-case keyword
1364 (FREQ (intern (upcase values)))
1365 (UNTIL (if (length> values 8)
1366 (ical:read-date-time values)
1367 (ical:read-date values)))
1368 ((COUNT INTERVAL)
1369 (string-to-number values))
1370 ((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
1371 (ical:read-list-with #'string-to-number values nil ","))
1372 (BYDAY
1373 (ical:read-list-with #'ical:read-weekdaynum values
1374 (rx ical:weekdaynum) ","))
1375 (WKST (cdr (assoc values ical:weekday-numbers)))))))
1376
1377(defun ical:print-recur-rule-part (part)
1378 "Serialize recur rule part PART to a string."
1379 (let ((keyword (car part))
1380 (values (cadr part))
1381 values-str)
1382 (cl-case keyword
1383 (FREQ (setq values-str (symbol-name values)))
1384 (UNTIL (setq values-str (cl-typecase values
1385 (ical:date-time (ical:print-date-time values))
1386 (ical:date (ical:print-date values)))))
1387 ((COUNT INTERVAL)
1388 (setq values-str (number-to-string values)))
1389 ((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
1390 (setq values-str (string-join (mapcar #'number-to-string values)
1391 ",")))
1392 (BYDAY
1393 (setq values-str (string-join (mapcar #'ical:print-weekdaynum values)
1394 ",")))
1395 (WKST (setq values-str (car (rassq values ical:weekday-numbers)))))
1396
1397 (concat (symbol-name keyword) "=" values-str)))
1398
1399(rx-define ical:recur-rule-part
1400 ;; Group 11: keyword
1401 ;; Group 12: value(s)
1402 (or (seq (group-n 11 "FREQ") "=" (group-n 12 ical:freq))
1403 (seq (group-n 11 "UNTIL") "=" (group-n 12 (or ical:date-time ical:date)))
1404 (seq (group-n 11 "COUNT") "=" (group-n 12 (one-or-more digit)))
1405 (seq (group-n 11 "INTERVAL") "=" (group-n 12 (one-or-more digit)))
1406 (seq (group-n 11 "BYSECOND") "=" (group-n 12 ; 0 to 60
1407 (ical:comma-list (** 1 2 digit))))
1408 (seq (group-n 11 "BYMINUTE") "=" (group-n 12 ; 0 to 59
1409 (ical:comma-list (** 1 2 digit))))
1410 (seq (group-n 11 "BYHOUR") "=" (group-n 12 ; 0 to 23
1411 (ical:comma-list (** 1 2 digit)))) ; 0 to 23
1412 (seq (group-n 11 "BYDAY") "=" (group-n 12 ; weeknum? daynum, e.g. SU or 34SU
1413 (ical:comma-list ical:weekdaynum)))
1414 (seq (group-n 11 "BYMONTHDAY") "=" (group-n 12
1415 (ical:comma-list ical:monthdaynum)))
1416 (seq (group-n 11 "BYYEARDAY") "=" (group-n 12
1417 (ical:comma-list ical:yeardaynum)))
1418 (seq (group-n 11 "BYWEEKNO") "=" (group-n 12 (ical:comma-list ical:weeknum)))
1419 (seq (group-n 11 "BYMONTH") "=" (group-n 12 (ical:comma-list ical:monthnum)))
1420 (seq (group-n 11 "BYSETPOS") "=" (group-n 12
1421 (ical:comma-list ical:yeardaynum)))
1422 (seq (group-n 11 "WKST") "=" (group-n 12 ical:weekday))))
1423
1424(defun ical:read-recur (s)
1425 "Read a recurrence rule value from string S.
1426S should be a match against rx `icalendar-recur'."
1427 ;; TODO: let's switch to keywords and a plist, so we can more easily
1428 ;; write these clauses also in diary sexp entries without so many parens
1429 (ical:read-list-with #'ical:read-recur-rule-part s (rx ical:recur-rule-part) ";"))
1430
1431(defun ical:print-recur (val)
1432 "Serialize a recurrence rule value VAL to a string."
1433 ;; RFC5545 sec. 3.3.10: "to ensure backward compatibility with
1434 ;; applications that pre-date this revision of iCalendar the
1435 ;; FREQ rule part MUST be the first rule part specified in a
1436 ;; RECUR value."
1437 (string-join
1438 (cons
1439 (ical:print-recur-rule-part (assq 'FREQ val))
1440 (mapcar #'ical:print-recur-rule-part
1441 (seq-filter (lambda (part) (not (eq 'FREQ (car part))))
1442 val)))
1443 ";"))
1444
1445(defconst ical:-recur-value-types
1446 ;; `list-of' is not a cl-type specifier, just a symbol here; it is
1447 ;; handled specially when checking types in `ical:recur-value-p':
1448 '(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
1449 UNTIL (or ical:date-time ical:date)
1450 COUNT (integer 1 *)
1451 INTERVAL (integer 1 *)
1452 BYSECOND (list-of (integer 0 60))
1453 BYMINUTE (list-of (integer 0 59))
1454 BYHOUR (list-of (integer 0 23))
1455 BYDAY (list-of (or (integer 0 6) (satisfies ical:dayno-offset-p)))
1456 BYMONTHDAY (list-of (or (integer -31 -1) (integer 1 31)))
1457 BYYEARDAY (list-of (or (integer -366 -1) (integer 1 366)))
1458 BYWEEKNO (list-of (or (integer -53 -1) (integer 1 53)))
1459 BYMONTH (list-of (integer 1 12)) ; unlike the others, months cannot be negative
1460 BYSETPOS (list-of (or (integer -366 -1) (integer 1 366)))
1461 WKST (integer 0 6))
1462 "Plist mapping `icalendar-recur' keywords to type specifiers.")
1463
1464(defun ical:dayno-offset-p (val)
1465 "Return non-nil if VAL is a pair (DAYNO . OFFSET).
1466DAYNO must be in [0..6] and OFFSET in [-53..53], excluding 0."
1467 (and (consp val)
1468 (cl-typep (car val) '(integer 0 6))
1469 (cl-typep (cdr val) '(or (integer -53 -1) (integer 1 53)))))
1470
1471(defun ical:recur-value-p (vals)
1472 "Return non-nil if VALS is an iCalendar recurrence rule value."
1473 (and (listp vals)
1474 ;; FREQ is always required:
1475 (assq 'FREQ vals)
1476 ;; COUNT and UNTIL are mutually exclusive if present:
1477 (not (and (assq 'COUNT vals) (assq 'UNTIL vals)))
1478 ;; If BYSETPOS is present, another BYXXX clause must be too:
1479 (or (not (assq 'BYSETPOS vals))
1480 (assq 'BYMONTH vals)
1481 (assq 'BYWEEKNO vals)
1482 (assq 'BYYEARDAY vals)
1483 (assq 'BYMONTHDAY vals)
1484 (assq 'BYDAY vals)
1485 (assq 'BYHOUR vals)
1486 (assq 'BYMINUTE vals)
1487 (assq 'BYSECOND vals))
1488 (let ((freq (ical:recur-freq vals))
1489 (byday (ical:recur-by* 'BYDAY vals))
1490 (byweekno (ical:recur-by* 'BYWEEKNO vals))
1491 (bymonthday (ical:recur-by* 'BYMONTHDAY vals))
1492 (byyearday (ical:recur-by* 'BYYEARDAY vals)))
1493 (and
1494 ;; "The BYDAY rule part MUST NOT be specified with a numeric
1495 ;; value when the FREQ rule part is not set to MONTHLY or
1496 ;; YEARLY."
1497 (or (not (consp (car byday)))
1498 (memq freq '(MONTHLY YEARLY)))
1499 ;; "The BYDAY rule part MUST NOT be specified with a numeric
1500 ;; value with the FREQ rule part set to YEARLY when the
1501 ;; BYWEEKNO rule part is specified." This also covers:
1502 ;; "[The BYWEEKNO] rule part MUST NOT be used when the FREQ
1503 ;; rule part is set to anything other than YEARLY."
1504 (or (not byweekno)
1505 (and (eq freq 'YEARLY)
1506 (not (consp (car byday)))))
1507 ;; "The BYMONTHDAY rule part MUST NOT be specified when the
1508 ;; FREQ rule part is set to WEEKLY."
1509 (not (and bymonthday (eq freq 'WEEKLY)))
1510 ;; "The BYYEARDAY rule part MUST NOT be specified when the
1511 ;; FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."
1512 (not (and byyearday (memq freq '(DAILY WEEKLY MONTHLY))))))
1513 ;; check types of all rule parts:
1514 (seq-every-p
1515 (lambda (kv)
1516 (when (consp kv)
1517 (let* ((keyword (car kv))
1518 (val (cadr kv))
1519 (type (plist-get ical:-recur-value-types keyword)))
1520 (and keyword val type
1521 (if (and (consp type)
1522 (eq (car type) 'list-of))
1523 (ical:list-of-p val (cadr type))
1524 (cl-typep val type))))))
1525 vals)))
1526
1527(ical:define-type ical:recur "RECUR"
1528 "Type for Recurrence Rule values.
1529
1530When printed, a recurrence rule value looks like
1531 KEY1=VAL1;KEY2=VAL2;...
1532where the VALs may themselves be lists or have other syntactic
1533structure; see RFC5545 sec. 3.3.10 for all the details.
1534
1535The KEYs and their associated value types when read are as follows.
1536The first is required:
1537 '(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
1538These two are mutually exclusive; at most one may appear:
1539 UNTIL (or icalendar-date-time icalendar-date)
1540 COUNT (integer 1 *)
1541All others are optional:
1542 INTERVAL (integer 1 *)
1543 BYSECOND (list-of (integer 0 60))
1544 BYMINUTE (list-of (integer 0 59))
1545 BYHOUR (list-of (integer 0 23))
1546 BYDAY (list-of (or (integer 0 6) ; day of week
1547 (pair (integer 0 6) ; (day of week . offset)
1548 (integer -53 53))) ; except 0
1549 BYMONTHDAY (list-of (integer -31 31)) ; except 0
1550 BYYEARDAY (list-of (integer -366 366)) ; except 0
1551 BYWEEKNO (list-of (integer -53 53)) ; except 0
1552 BYMONTH (list-of (integer 1 12)) ; months cannot be negative
1553 BYSETPOS (list-of (integer -366 366)) ; except 0
1554 WKST (integer 0 6))
1555
1556When read, these KEYs and their associated VALs are gathered into
1557an alist.
1558
1559In general, the VALs consist of integers or lists of integers.
1560Abbreviations for weekday names are translated into integers
15610 (=Sunday) through 6 (=Saturday), for compatibility with
1562calendar.el and decoded-time-* functions.
1563
1564Some examples:
1565
15661) Printed: FREQ=DAILY;COUNT=10;INTERVAL=2
1567 Meaning: 10 occurrences that occur every other day
1568 Read: ((FREQ DAILY)
1569 (COUNT 10)
1570 (INTERVAL 2))
1571
15722) Printed: FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
1573 Meaning: Every day in January of every year until 2000/01/31 at 14:00 UTC
1574 Read: ((FREQ YEARLY)
1575 (UNTIL (0 0 14 31 1 2000 1 -1 0))
1576 (BYMONTH (1))
1577 (BYDAY (0 1 2 3 4 5 6)))
1578
15793) Printed: FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2
1580 Meaning: Every month on the second-to-last weekday of the month
1581 Read: ((FREQ MONTHLY)
1582 (BYDAY (1 2 3 4 5))
1583 (BYSETPOS (-2)))
1584
1585Notice that singleton values are still wrapped in a list when the
1586KEY accepts a list of values, but not when the KEY always has a
1587single (e.g. integer) value."
1588 '(satisfies ical:recur-value-p)
1589 (ical:semicolon-list ical:recur-rule-part)
1590 :reader ical:read-recur
1591 :printer ical:print-recur
1592 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10")
1593
1594(defun ical:recur-freq (recur-value)
1595 "Return the frequency in RECUR-VALUE."
1596 (car (alist-get 'FREQ recur-value)))
1597
1598(defun ical:recur-interval-size (recur-value)
1599 "Return the interval size in RECUR-VALUE, or the default of 1."
1600 (or (car (alist-get 'INTERVAL recur-value)) 1))
1601
1602(defun ical:recur-until (recur-value)
1603 "Return the UNTIL date(-time) in RECUR-VALUE."
1604 (car (alist-get 'UNTIL recur-value)))
1605
1606(defun ical:recur-count (recur-value)
1607 "Return the COUNT in RECUR-VALUE."
1608 (car (alist-get 'COUNT recur-value)))
1609
1610(defun ical:recur-weekstart (recur-value)
1611 "Return the weekday which starts the work week in RECUR-VALUE.
1612If no starting weekday is specified in RECUR-VALUE, returns the default,
16131 (= Monday)."
1614 (or (car (alist-get 'WKST recur-value)) 1))
1615
1616(defun ical:recur-by* (byunit recur-value)
1617 "Return the values in the BYUNIT clause in RECUR-VALUE.
1618BYUNIT should be a symbol: \\='BYMONTH, \\='BYDAY, etc.
1619See `icalendar-recur' for all the possible BYUNIT values."
1620 (car (alist-get byunit recur-value)))
1621
1622;;;; 3.3.11 Text
1623(rx-define ical:escaped-char
1624 (seq ?\\ (or ?\\ ?\; ?, ?N ?n)))
1625
1626(rx-define ical:text-safe-char
1627 ;; "Any character except CONTROLs not needed by the current character
1628 ;; set, DQUOTE, ";", ":", "\", "," "
1629 (any #x09 #x20 #x21 ; htab, space, and "!"
1630 (#x23 . #x2B) (#x2D . #x39) ; "#".."9" skipping #x2C=","
1631 (#x3C . #x5B) (#x5D . #x7E) ; "<".."~" skipping #x5C="\"
1632 nonascii))
1633
1634(defun ical:text-region-p (val)
1635 "Return t if VAL represents a region of text."
1636 (and (listp val)
1637 (markerp (car val))
1638 (not (null (marker-buffer (car val))))
1639 (markerp (cdr val))))
1640
1641(defun ical:make-text-region (&optional buffer begin end)
1642 "Return an object that represents a region of text.
1643The region is taken from BUFFER between BEGIN and END. BUFFER defaults
1644to the current buffer, and BEGIN and END default to point and mark in
1645BUFFER."
1646 (let ((buf (or buffer (current-buffer)))
1647 (b (make-marker))
1648 (e (make-marker)))
1649 (with-current-buffer buf
1650 (set-marker b (or begin (region-beginning)) buf)
1651 (set-marker e (or end (region-end)))
1652 (cons b e))))
1653
1654(defsubst ical:text-region-begin (r)
1655 "Return the marker at the beginning of the text region R."
1656 (car r))
1657
1658(defsubst ical:text-region-end (r)
1659 "Return the marker at the end of the text region R."
1660 (cdr r))
1661
1662(defun ical:unescape-text-in-region (begin end)
1663 "Unescape the text between BEGIN and END.
1664Unescaping replaces literal '\\n' and '\\N' with newline, and removes
1665backslashes that escape commas, semicolons, and backslashes."
1666 (with-restriction begin end
1667 (save-excursion
1668 (replace-string-in-region "\\N" "\n" (point-min) (point-max))
1669 (replace-string-in-region "\\n" "\n" (point-min) (point-max))
1670 (replace-string-in-region "\\," "," (point-min) (point-max))
1671 (replace-string-in-region "\\;" ";" (point-min) (point-max)))
1672 (replace-string-in-region (concat "\\" "\\") "\\" (point-min) (point-max))))
1673
1674(defun ical:unescape-text-string (s)
1675 "Unescape the text in string S.
1676Unescaping replaces literal '\\n' and '\\N' with newline, and removes
1677backslashes that escape commas, semicolons, and backslashes."
1678 (with-temp-buffer
1679 (insert s)
1680 (ical:unescape-text-in-region (point-min) (point-max))
1681 (buffer-string)))
1682
1683(defun ical:escape-text-in-region (begin end)
1684 "Escape the text between BEGIN and END in the current buffer.
1685Escaping replaces newlines with literal '\\n', and escapes commas,
1686semicolons and backslashes with a backslash."
1687 (with-restriction begin end
1688 (save-excursion
1689 ;; replace backslashes first, so the ones introduced when
1690 ;; escaping other characters don't end up double-escaped:
1691 (replace-string-in-region "\\" (concat "\\" "\\") (point-min) (point-max))
1692 (replace-string-in-region "\n" "\\n" (point-min) (point-max))
1693 (replace-string-in-region "," "\\," (point-min) (point-max))
1694 (replace-string-in-region ";" "\\;" (point-min) (point-max)))))
1695
1696(defun ical:escape-text-string (s)
1697 "Escape the text in string S.
1698Escaping replaces newlines with literal '\\n', and escapes commas,
1699semicolons and backslashes with a backslash."
1700 (with-temp-buffer
1701 (insert s)
1702 (ical:escape-text-in-region (point-min) (point-max))
1703 (buffer-string)))
1704
1705(defun ical:read-text (s)
1706 "Read an `icalendar-text' value from a string S.
1707S should be a match against rx `icalendar-text'."
1708 (ical:unescape-text-string s))
1709
1710(defun ical:print-text (val)
1711 "Serialize an iCalendar text value.
1712VAL may be a string or text region (see `icalendar-make-text-region').
1713The text will be escaped before printing. If VAL is a region, the text
1714it contains will not be modified; it is copied before escaping."
1715 (if (stringp val)
1716 (ical:escape-text-string val)
1717 ;; val is a region, so copy and escape its contents:
1718 (let* ((beg (ical:text-region-begin val))
1719 (buf (marker-buffer beg))
1720 (end (ical:text-region-end val)))
1721 (with-temp-buffer
1722 (insert-buffer-substring buf (marker-position beg) (marker-position end))
1723 (ical:escape-text-in-region (point-min) (point-max))
1724 (buffer-string)))))
1725
1726(defun ical:text-to-string (node)
1727 "Return the value of an `icalendar-text' NODE as a string.
1728The returned string is *not* escaped. For that, see `icalendar-print-text'."
1729 (ical:with-node-value node nil
1730 (if (stringp value) value
1731 ;; Otherwise the value is a text region:
1732 (let* ((beg (ical:text-region-begin value))
1733 (buf (marker-buffer beg))
1734 (end (ical:text-region-end value)))
1735 (with-current-buffer buf
1736 (buffer-substring (marker-position beg) (marker-position end)))))))
1737
1738;; TODO: would it be useful to add a third representation, namely a
1739;; function or thunk? So that e.g. Org can pre-process its own syntax
1740;; and return a plain text string to use in the description?
1741(ical:define-type ical:text "TEXT"
1742 "Type for Text values.
1743
1744Text values can be represented in Elisp in two ways: as strings,
1745or as buffer regions. For values which aren't expected to change,
1746such as property values in a text/calendar email attachment, use
1747strings. For values which are user-editable and might change
1748between parsing and serializing to iCalendar format, use a
1749region. In that case, a text value contains two markers BEGIN and
1750END which mark the bounds of the region. See
1751`icalendar-make-text-region' to create such values, and
1752`icalendar-text-region-begin' and `icalendar-text-region-end' to
1753access the markers.
1754
1755Certain characters in text values are required to be escaped by
1756the iCalendar standard. These characters should NOT be
1757pre-escaped when inserting them into the parse tree. Instead,
1758`icalendar-print-text' takes care of escaping text values, and
1759`icalendar-read-text' takes care of unescaping them, when parsing and
1760printing iCalendar data."
1761 '(or string (satisfies ical:text-region-p))
1762 (zero-or-more (or ical:text-safe-char ?: ?\" ical:escaped-char))
1763 :reader ical:read-text
1764 :printer ical:print-text
1765 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.11")
1766
1767;; 3.3.12 Time - Defined above
1768
1769;;;; 3.3.13 URI
1770;; see https://www.rfc-editor.org/rfc/rfc3986#section-3
1771(rx-define ical:uri-with-scheme
1772 ;; Group 11: URI scheme; see icalendar-uri-schemes.el
1773 ;; Group 12: rest of URI after ":"
1774 ;; This regex mostly just scans for all characters allowed by RFC3986,
1775 ;; except we make an effort to parse the scheme, because otherwise the
1776 ;; regex is either too permissive (ical:binary, in particular, matches
1777 ;; a subset of the characters allowed in a URI) or too complicated to
1778 ;; be useful.
1779 ;; TODO: use url-parse.el to parse to struct?
1780 (seq (group-n 11 (any "a-zA-Z") (zero-or-more (any ?- ?+ ?. "A-Za-z0-9")))
1781 ":"
1782 (group-n 12
1783 (one-or-more
1784 (any "A-Za-z0-9" ?- ?. ?_ ?~ ; unreserved chars
1785 ?: ?/ ?? ?# ?\[ ?\] ?@ ; gen-delims
1786 ?! ?$ ?& ?' ?\( ?\) ?* ?+ ?, ?\; ?= ; sub-delims
1787 ?%))))) ; for %-encoding
1788
1789(ical:define-type ical:uri "URI"
1790 "Type for URI values.
1791
1792The parsed and printed representations are the same: a URI string."
1793 '(satisfies ical:match-uri-value)
1794 ical:uri-with-scheme
1795 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.13")
1796
1797;;;; 3.3.3 Calendar User Address
1798(ical:define-type ical:cal-address "CAL-ADDRESS"
1799 "Type for Calendar User Address values.
1800
1801The parsed and printed representations are the same: a URI string.
1802Typically, this should be a \"mailto:\" URI.
1803
1804RFC5545 says: \"*When used to address an Internet email transport
1805 address* for a calendar user, the value MUST be a mailto URI,
1806 as defined by [RFC2368]\"
1807
1808Since it is unclear whether there are Calendar User Address values
1809which are not used to address email, this type does not enforce the use
1810of the mailto: scheme, but be prepared for problems if you create
1811values of this type with any other scheme."
1812 '(and string (satisfies ical:match-cal-address-value))
1813 ical:uri-with-scheme
1814 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.3")
1815
1816;;;; 3.3.14 UTC Offset
1817(defun ical:read-utc-offset (s)
1818 "Read a UTC offset from a string.
1819S should be a match against rx `icalendar-utc-offset'"
1820 (let ((sign (if (equal (substring s 0 1) "-") -1 1))
1821 (nhours (string-to-number (substring s 1 3)))
1822 (nminutes (string-to-number (substring s 3 5)))
1823 (nseconds (if (length= s 7)
1824 (string-to-number (substring s 5 7))
1825 0)))
1826 (* sign (+ nseconds (* 60 nminutes) (* 60 60 nhours)))))
1827
1828(defun ical:print-utc-offset (utcoff)
1829 "Serialize a UTC offset to a string."
1830 (let* ((sign (if (< utcoff 0) "-" "+"))
1831 (absoff (abs utcoff))
1832 (nhours (/ absoff (* 60 60)))
1833 (no-hours (mod absoff (* 60 60)))
1834 (nminutes (/ no-hours 60))
1835 (nseconds (mod no-hours 60)))
1836 (if (zerop nseconds)
1837 (format "%s%02d%02d" sign nhours nminutes)
1838 (format "%s%02d%02d%02d" sign nhours nminutes nseconds))))
1839
1840(ical:define-type ical:utc-offset "UTC-OFFSET"
1841 "Type for UTC Offset values.
1842
1843When printed, a sign followed by a string of digits, like +HHMM
1844or -HHMMSS. When read, an integer representing the number of
1845seconds offset from UTC. This representation is for compatibility
1846with `decode-time' and related functions."
1847 '(integer -999999 999999)
1848 (seq (or ?+ ?-) ; + is not optional for positive values!
1849 (= 4 digit) ; HHMM
1850 (zero-or-one (= 2 digit))) ; SS
1851 :reader ical:read-utc-offset
1852 :printer ical:print-utc-offset
1853 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.14")
1854
1855
1856;;; Section 3.2: Property Parameters
1857
1858(defvar ical:param-types nil ;; populated by ical:define-param
1859 "Alist mapping printed parameter names to type symbols.")
1860
1861(defun ical:maybe-quote-param-value (s &optional always)
1862 "Add quotes around param value string S if required.
1863If ALWAYS is non-nil, add quotes to S regardless of its contents."
1864 (if (or always
1865 (not (string-match (rx ical:paramtext) s))
1866 (< (match-end 0) (length s)))
1867 (concat "\"" s "\"")
1868 s))
1869
1870(defun ical:read-param-value (type s)
1871 "Read a value for a parameter of type TYPE from a string S.
1872S should have already been matched against the regex for TYPE and
1873the match data should be available to this function. Returns a
1874syntax node of type TYPE containing the read value.
1875
1876If TYPE accepts a list of values, S will be split on the list
1877separator for TYPE and read individually."
1878 (let* ((value-type (get type 'ical:value-type)) ; if nil, value is just a string
1879 (value-regex (when (get type 'ical:value-rx)
1880 (rx-to-string (get type 'ical:value-rx))))
1881 (list-sep (get type 'ical:list-sep))
1882 (substitute-val (get type 'ical:substitute-value))
1883 (unrecognized-val (match-string 5)) ; see :unrecognized in define-param
1884 (raw-val (if unrecognized-val substitute-val s))
1885 (one-val-reader (if (ical:value-type-symbol-p value-type)
1886 (lambda (s) (ical:read-value-node value-type s))
1887 #'identity)) ; value is just a string
1888 ;; values may be quoted even if :quoted does not require it,
1889 ;; so they need to be stripped of quotes. read-list-with does
1890 ;; this by default; in the single value case, use string-trim
1891 (read-val (if list-sep
1892 (ical:read-list-with one-val-reader raw-val
1893 value-regex list-sep)
1894 (funcall one-val-reader
1895 (string-trim raw-val "\"" "\"")))))
1896 (ical:make-ast-node type
1897 (list :value read-val
1898 :original-value unrecognized-val))))
1899
1900(defun ical:parse-param-value (type limit)
1901 "Parse the value for a parameter of type TYPE from point up to LIMIT.
1902TYPE should be a type symbol for an iCalendar parameter type.
1903This function expects point to be at the start of the value
1904string, after the parameter name and the equals sign. Returns a
1905syntax node representing the parameter."
1906 (let ((full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
1907 ;; By far the most common invalid data seem to be text values that
1908 ;; contain unescaped characters (e.g. commas in addresses). These
1909 ;; are harmless as long as the parameter accepts arbitrary text and
1910 ;; does not expect a list of values. The only such parameter
1911 ;; defined in RFC5545 is `ical:cnparam', so we treat this as a
1912 ;; special case and loosen the official regexp to accept anything up
1913 ;; to the start of the next param or property value:
1914 (when (and (eq type 'ical:cnparam)
1915 (not ical:parse-strictly))
1916 (setq full-value-regex
1917 (rx (group-n 2 (or ical:quoted-string
1918 (zero-or-more (not (any ?: ?\;))))))))
1919
1920 (unless (re-search-forward full-value-regex limit t)
1921 (ical:signal-parse-error
1922 (format "Unable to parse `%s' value between %d and %d"
1923 type (point) limit)))
1924 (when (match-string 3)
1925 (ical:signal-parse-error
1926 (format "Invalid value for `%s' parameter: %s" type (match-string 3))))
1927
1928 (let ((value-begin (match-beginning 2))
1929 (value-end (match-end 2))
1930 (node (ical:read-param-value type (match-string 2))))
1931 (ical:ast-node-meta-set node :buffer (current-buffer))
1932 ;; :begin must be set by parse-params
1933 (ical:ast-node-meta-set node :value-begin value-begin)
1934 (ical:ast-node-meta-set node :value-end value-end)
1935 (ical:ast-node-meta-set node :end value-end)
1936
1937 node)))
1938
1939(defun ical:parse-params (limit)
1940 "Parse the parameter string of the current property, up to LIMIT.
1941Point should be at the \";\" at the start of the first parameter.
1942Returns a list of parameters, which may be nil if none are present.
1943After parsing, point is at the end of the parameter string and the
1944start of the property value string."
1945 (let (params param-node)
1946 (rx-let ((ical:param-start (seq ";" (group-n 1 ical:param-name) "=")))
1947 (while (re-search-forward (rx ical:param-start) limit t)
1948 (when-let* ((begin (match-beginning 1))
1949 (param-name (match-string 1))
1950 (param-type (alist-get (upcase param-name)
1951 ical:param-types
1952 'ical:otherparam
1953 nil #'equal)))
1954 (condition-case err
1955 (setq param-node (ical:parse-param-value param-type limit))
1956 (ical:parse-error
1957 (ical:handle-parse-error err (format "Skipping bad %s parameter"
1958 param-name))
1959 (setq param-node nil)))
1960 (when param-node
1961 (ical:ast-node-meta-set param-node :begin begin)
1962 ;; store the original param name if we didn't recognize it:
1963 (when (eq param-type 'ical:otherparam)
1964 (ical:ast-node-meta-set param-node :original-name param-name))
1965 (push param-node params))))
1966 (nreverse params))))
1967
1968(defun ical:print-param-node (node)
1969 "Serialize a parameter syntax node NODE to a string.
1970NODE should be a syntax node whose type is an iCalendar
1971parameter type."
1972 (let* ((param-type (ical:ast-node-type node))
1973 (param-name (car (rassq param-type ical:param-types)))
1974 (name-str (or param-name
1975 ;; set by parse-params for unrecognized params:
1976 (ical:ast-node-meta-get :original-name node))))
1977
1978 (unless (and name-str (stringp name-str) (not (equal name-str "")))
1979 (ical:signal-print-error "No printable parameter name" :node node))
1980
1981 (let* ((list-sep (get param-type 'ical:list-sep))
1982 (val/s (ical:ast-node-value node))
1983 (vals (if (and list-sep (listp val/s))
1984 val/s
1985 (list val/s)))
1986 ;; any ical:print-error here propagates:
1987 (printed (mapcar #'ical:default-value-printer vals))
1988 ;; add quotes to each value as needed, even if :quoted
1989 ;; does not require it:
1990 (must-quote (get param-type 'ical:is-quoted))
1991 (quoted (mapcar
1992 (lambda (v) (ical:maybe-quote-param-value v must-quote))
1993 printed))
1994 (val-str (or (ical:ast-node-meta-get :original-value node)
1995 (string-join quoted list-sep)
1996 quoted)))
1997
1998 (unless (and (stringp val-str) (not (equal val-str "")))
1999 (ical:signal-print-error "Unable to print parameter value" :node node))
2000
2001 (format ";%s=%s" name-str val-str))))
2002
2003(defun ical:print-params (param-nodes)
2004 "Print the property parameter nodes in PARAM-NODES.
2005Returns the printed parameter list as a string."
2006 (let (param-strs)
2007 (dolist (node param-nodes)
2008 (condition-case err
2009 (push (ical:print-param-node node) param-strs)
2010 (ical:print-error
2011 (ical:handle-print-error err))))
2012 (apply #'concat (nreverse param-strs))))
2013
2014;; Parameter definitions in RFC5545:
2015
2016(ical:define-param ical:altrepparam "ALTREP"
2017 "Alternate text representation (URI)"
2018 ical:uri
2019 :quoted t
2020 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.1")
2021
2022(ical:define-param ical:cnparam "CN"
2023 "Common Name"
2024 ical:param-value
2025 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.2")
2026
2027(ical:define-param ical:cutypeparam "CUTYPE"
2028 "Calendar User Type"
2029 (or "INDIVIDUAL"
2030 "GROUP"
2031 "RESOURCE"
2032 "ROOM"
2033 "UNKNOWN"
2034 (group-n 5
2035 (or ical:x-name ical:iana-token)))
2036 :default "INDIVIDUAL"
2037 ;; "Applications MUST treat x-name and iana-token values they
2038 ;; don't recognize the same way as they would the UNKNOWN
2039 ;; value":
2040 :unrecognized "UNKNOWN"
2041 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.3")
2042
2043(ical:define-param ical:delfromparam "DELEGATED-FROM"
2044 "Delegators.
2045
2046This is a comma-separated list of quoted `icalendar-cal-address' URIs,
2047typically specified on the `icalendar-attendee' property. The users in
2048this list have delegated their participation to the user which is
2049the value of the property."
2050 ical:cal-address
2051 :quoted t
2052 :list-sep ","
2053 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.4")
2054
2055(ical:define-param ical:deltoparam "DELEGATED-TO"
2056 "Delegatees.
2057
2058This is a comma-separated list of quoted `icalendar-cal-address' URIs,
2059typically specified on the `icalendar-attendee' property. The users in
2060this list have been delegated to participate by the user which is
2061the value of the property."
2062 ical:cal-address
2063 :quoted t
2064 :list-sep ","
2065 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.5")
2066
2067(ical:define-param ical:dirparam "DIR"
2068 "Directory Entry Reference.
2069
2070This parameter may be specified on properties with a
2071`icalendar-cal-address' value type. It is a quoted URI which specifies
2072a reference to a directory entry associated with the calendar
2073user which is the value of the property."
2074 ical:uri
2075 :quoted t
2076 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.6")
2077
2078(ical:define-param ical:encodingparam "ENCODING"
2079 "Inline Encoding, either \"8BIT\" (text, default) or \"BASE64\" (binary).
2080
2081If \"BASE64\", the property value is base64-encoded binary data.
2082This parameter must be specified if the `icalendar-valuetypeparam'
2083is \"BINARY\"."
2084 (or "8BIT" "BASE64")
2085 :default "8BIT"
2086 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.7")
2087
2088(rx-define ical:mimetype
2089 (seq ical:mimetype-regname "/" ical:mimetype-regname))
2090
2091;; from https://www.rfc-editor.org/rfc/rfc4288#section-4.2:
2092(rx-define ical:mimetype-regname
2093 (** 1 127 (any "A-Za-z0-9" ?! ?# ?$ ?& ?. ?+ ?- ?^ ?_)))
2094
2095(ical:define-param ical:fmttypeparam "FMTTYPE"
2096 "Format Type (Mimetype per RFC4288)
2097
2098Specifies the media type of the object referenced in the property value,
2099for example \"text/plain\" or \"text/html\".
2100Valid media types are defined in RFC4288; see
2101URL `https://www.rfc-editor.org/rfc/rfc4288#section-4.2'"
2102 ical:mimetype
2103 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.8")
2104
2105(ical:define-param ical:fbtypeparam "FBTYPE"
2106 "Free/Busy Time Type. Default is \"BUSY\".
2107
2108RFC5545 gives the following meanings to the values:
2109
2110FREE: the time interval is free for scheduling.
2111BUSY: the time interval is busy because one or more events have
2112 been scheduled for that interval.
2113BUSY-UNAVAILABLE: the time interval is busy and the interval
2114 can not be scheduled.
2115BUSY-TENTATIVE: the time interval is busy because one or more
2116 events have been tentatively scheduled for that interval.
2117Other values are treated like BUSY."
2118 (or "FREE"
2119 "BUSY-UNAVAILABLE"
2120 "BUSY-TENTATIVE"
2121 "BUSY"
2122 ical:x-name
2123 ical:iana-token)
2124 :default "BUSY"
2125 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.9")
2126
2127;; TODO: see https://www.rfc-editor.org/rfc/rfc5646#section-2.1
2128(rx-define ical:rfc5646-lang
2129 (one-or-more (any "A-Za-z0-9" ?-)))
2130
2131(ical:define-param ical:languageparam "LANGUAGE"
2132 "Language tag (per RFC5646)
2133
2134This parameter specifies the language of the property value as a
2135language tag, for example \"en-US\" for US English or \"no\" for
2136Norwegian. Valid language tags are defined in RFC5646; see
2137URL `https://www.rfc-editor.org/rfc/rfc5646'"
2138 ical:rfc5646-lang
2139 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.10")
2140
2141(ical:define-param ical:memberparam "MEMBER"
2142 "Group or List Membership.
2143
2144This is a comma-separated list of quoted `icalendar-cal-address'
2145values. These are addresses of groups or lists of which the user
2146in the property value is a member."
2147 ical:cal-address
2148 :quoted t
2149 :list-sep ","
2150 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.11")
2151
2152(ical:define-param ical:partstatparam "PARTSTAT"
2153 "Participation status.
2154
2155The value specifies the participation status of the calendar user
2156in the property value. They have different interpretations
2157depending on whether they occur in a VEVENT, VTODO or VJOURNAL
2158component. RFC5545 gives the values the following meanings:
2159
2160NEEDS-ACTION (all): needs action by the user
2161ACCEPTED (all): accepted by the user
2162DECLINED (all): declined by the user
2163TENTATIVE (VEVENT, VTODO): tentatively accepted by the user
2164DELEGATED (VEVENT, VTODO): delegated by the user
2165COMPLETED (VTODO): completed at the `icalendar-date-time' in the
2166 VTODO's `icalendar-completed' property
2167IN-PROCESS (VTODO): in the process of being completed"
2168 (or "NEEDS-ACTION"
2169 "ACCEPTED"
2170 "DECLINED"
2171 "TENTATIVE"
2172 "DELEGATED"
2173 "COMPLETED"
2174 "IN-PROCESS"
2175 (group-n 5 (or ical:x-name
2176 ical:iana-token)))
2177 ;; "Applications MUST treat x-name and iana-token values
2178 ;; they don't recognize the same way as they would the
2179 ;; NEEDS-ACTION value."
2180 :default "NEEDS-ACTION"
2181 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.12")
2182
2183(ical:define-param ical:rangeparam "RANGE"
2184 "Recurrence Identifier Range.
2185
2186Specifies the effective range of recurrence instances of the property's value.
2187The value \"THISANDFUTURE\" is the only value compliant with RFC5545;
2188legacy applications might also produce \"THISANDPRIOR\"."
2189 "THISANDFUTURE"
2190 :default "THISANDFUTURE"
2191 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.13")
2192
2193(ical:define-param ical:trigrelparam "RELATED"
2194 "Alarm Trigger Relationship.
2195
2196This parameter may be specified on properties whose values give
2197an alarm trigger as an `icalendar-duration'. If the parameter
2198value is \"START\" (the default), the alarm triggers relative to
2199the start of the component; similarly for \"END\"."
2200 (or "START" "END")
2201 :default "START"
2202 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.14")
2203
2204(ical:define-param ical:reltypeparam "RELTYPE"
2205 "Relationship type.
2206
2207This parameter specifies a hierarchical relationship between the
2208calendar component referenced in a `icalendar-related-to'
2209property and the calendar component in which it occurs.
2210\"PARENT\" means the referenced component is superior to this
2211one, \"CHILD\" that the referenced component is subordinate to
2212this one, and \"SIBLING\" means they are peers."
2213 (or "PARENT"
2214 "CHILD"
2215 "SIBLING"
2216 (group-n 5 (or ical:x-name
2217 ical:iana-token)))
2218 ;; "Applications MUST treat x-name and iana-token values they don't
2219 ;; recognize the same way as they would the PARENT value."
2220 :default "PARENT"
2221 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.15")
2222
2223(ical:define-param ical:roleparam "ROLE"
2224 "Participation role.
2225
2226This parameter specifies the participation role of the calendar
2227user in the property value. RFC5545 gives the parameter values
2228the following meanings:
2229CHAIR: chair of the calendar entity
2230REQ-PARTICIPANT (default): user's participation is required
2231OPT-PARTICIPANT: user's participation is optional
2232NON-PARTICIPANT: user is copied for information purposes only"
2233 (or "CHAIR"
2234 "REQ-PARTICIPANT"
2235 "OPT-PARTICIPANT"
2236 "NON-PARTICIPANT"
2237 (group-n 5 (or ical:x-name
2238 ical:iana-token)))
2239 ;; "Applications MUST treat x-name and iana-token values
2240 ;; they don't recognize the same way as they would the
2241 ;; REQ-PARTICIPANT value."
2242 :default "REQ-PARTICIPANT"
2243 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.16")
2244
2245(ical:define-param ical:rsvpparam "RSVP"
2246 "RSVP expectation.
2247
2248This parameter is an `icalendar-boolean' which specifies whether
2249the calendar user in the property value is expected to reply to
2250the Organizer of a VEVENT or VTODO."
2251 ical:boolean
2252 :default "FALSE"
2253 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.17")
2254
2255(ical:define-param ical:sentbyparam "SENT-BY"
2256 "Sent by.
2257
2258This parameter specifies a calendar user that is acting on behalf
2259of the user in the property value."
2260 ;; "The parameter value MUST be a mailto URI as defined in [RFC2368]"
2261 ;; Weirdly, this is the only place in the standard I've seen "mailto:"
2262 ;; be *required* for a cal-address. We ignore this requirement because
2263 ;; coding around the exception is not worth it: it requires working
2264 ;; around the fact that two different types, the looser and the more
2265 ;; stringent cal-address, would need to have the same print name.
2266 ical:cal-address
2267 :quoted t
2268 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.18")
2269
2270(ical:define-param ical:tzidparam "TZID"
2271 "Time Zone identifier.
2272
2273This parameter identifies the VTIMEZONE component in the calendar
2274which should be used to interpret the time value given in the
2275property. The value of this parameter must be equal to the value
2276of the TZID property in that VTIMEZONE component; there must be
2277exactly one such component for every unique value of this
2278parameter in the calendar."
2279 (seq (zero-or-one "/") ical:paramtext)
2280 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.19")
2281
2282(defun ical:read-value-type (s)
2283 "Read a value type from string S.
2284S should contain the printed representation of a value type in a \"VALUE=...\"
2285property parameter. If S represents a known type in `icalendar-value-types',
2286it is read as the associated type symbol. Otherwise S is returned unchanged."
2287 (let ((type-assoc (assoc s ical:value-types)))
2288 (if type-assoc
2289 (cdr type-assoc)
2290 s)))
2291
2292(defun ical:print-value-type (type)
2293 "Print a value type TYPE.
2294TYPE should be an iCalendar type symbol naming a known value type
2295defined with `icalendar-define-type', or a string naming an
2296unknown type. If it is a symbol, return the associated printed
2297representation for the type from `icalendar-value-types'.
2298Otherwise return TYPE."
2299 (if (symbolp type)
2300 (car (rassq type ical:value-types))
2301 type))
2302
2303(ical:define-type ical:printed-value-type nil
2304 "Type to represent values of the `icalendar-valuetypeparam' parameter.
2305
2306When read, if the type named by the parameter is a known value
2307type in `icalendar-value-types', it is represented as a type
2308symbol for that value type. If it is an unknown value type, it is
2309represented as a string. When printed, a string is returned
2310unchanged; a type symbol is printed as the associated name in
2311`icalendar-value-types'.
2312
2313This is not a type defined by RFC5545; it is defined here to
2314facilitate parsing of the `icalendar-valuetypeparam' parameter."
2315 '(or string (satisfies ical:printable-value-type-symbol-p))
2316 (or "BINARY"
2317 "BOOLEAN"
2318 "CAL-ADDRESS"
2319 "DATE-TIME"
2320 "DATE"
2321 "DURATION"
2322 "FLOAT"
2323 "INTEGER"
2324 "PERIOD"
2325 "RECUR"
2326 "TEXT"
2327 "TIME"
2328 "URI"
2329 "UTC-OFFSET"
2330 ;; Note: "Applications MUST preserve the value data for x-name
2331 ;; and iana-token values that they don't recognize without
2332 ;; attempting to interpret or parse the value data." So in this
2333 ;; case we don't specify :default or :unrecognized in the
2334 ;; parameter definition, and we don't put the value in group 5;
2335 ;; the reader will just preserve whatever string matches here.
2336 ical:x-name
2337 ical:iana-token)
2338 :reader ical:read-value-type
2339 :printer ical:print-value-type)
2340
2341(ical:define-param ical:valuetypeparam "VALUE"
2342 "Property value data type.
2343
2344This parameter is used to specify the value type of the
2345containing property's value, if it is not of the default value
2346type."
2347 ical:printed-value-type
2348 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.20")
2349
2350(ical:define-param ical:otherparam nil ; don't add to ical:param-types
2351 "Parameter with an unknown name.
2352
2353This is not a parameter type defined by RFC5545; it represents
2354parameters with an unknown name (matching rx `icalendar-param-name')
2355whose values must be parsed and preserved but not further
2356interpreted."
2357 ical:param-value)
2358
2359(rx-define ical:other-param-safe
2360 ;; we use this rx to skip params when matching properties and
2361 ;; their values. Thus we *don't* capture the param names and param values
2362 ;; in numbered groups here, which would clobber the groups of the enclosing
2363 ;; expression.
2364 (seq ";"
2365 (or ical:iana-token ical:x-name)
2366 "="
2367 (ical:comma-list ical:param-value)))
2368
2369
2370;;; Properties:
2371
2372(defvar ical:property-types nil ;; populated by ical:define-property
2373 "Alist mapping printed property names to type symbols.")
2374
2375(defun ical:read-property-value (type s &optional params)
2376 "Read a value for the property type TYPE from a string S.
2377
2378TYPE should be a type symbol for an iCalendar property type
2379defined with `icalendar-define-property'. The property value is
2380assumed to be of TYPE's default value type, unless an
2381`icalendar-valuetypeparam' parameter appears in PARAMS, in which
2382case a value of that type will be read. S should have already
2383been matched against TYPE's value regex and the match data should
2384be available to this function. Returns a property syntax node of
2385type TYPE containing the read value and the list of PARAMS.
2386
2387If TYPE accepts lists of values, they will be split from S on the
2388list separator and read separately."
2389 (let* ((value-type (or (ical:value-type-from-params params)
2390 (get type 'ical:default-type)))
2391 (list-sep (get type 'ical:list-sep))
2392 (unrecognized-val (match-string 5))
2393 (raw-val (if unrecognized-val
2394 (get type 'ical:substitute-value)
2395 s))
2396 (value (if list-sep
2397 (ical:read-list-of value-type raw-val list-sep)
2398 (ical:read-value-node value-type raw-val))))
2399 (ical:make-ast-node type
2400 (list :value value
2401 :original-value unrecognized-val)
2402 params)))
2403
2404(defun ical:parse-property-value (type limit &optional params)
2405 "Parse a value for the property type TYPE from point up to LIMIT.
2406This function expects point to be at the start of the value
2407expression, after \"PROPERTY-NAME[PARAM...]:\". Returns a syntax
2408node of type TYPE containing the parsed value and the list of
2409PARAMS."
2410 (let ((start (point))
2411 (full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
2412
2413 ;; By far the most common invalid data seem to be text values that
2414 ;; contain unescaped characters (e.g. commas in addresses in
2415 ;; LOCATION). These are harmless as long as the property accepts
2416 ;; any text value, accepts no other types of values, and does not
2417 ;; expect a list of values. So we treat this as a special case and
2418 ;; loosen the regexp to accept any non-control character until eol:
2419 (when (and (eq 'ical:text (get type 'ical:default-type))
2420 (equal (rx-to-string 'ical:text t)
2421 (rx-to-string (get type 'ical:value-rx) t))
2422 (null (get type 'ical:other-types))
2423 (not (ical:expects-list-of-values-p type))
2424 (not ical:parse-strictly))
2425 (setq full-value-regex
2426 (rx (group-n 2 (zero-or-more (not (any control))))
2427 line-end)))
2428
2429 (unless (re-search-forward full-value-regex limit t)
2430 (ical:signal-parse-error
2431 (format "Unable to parse `%s' property value between %d and %d"
2432 type start limit)
2433 :restart-at (1+ limit)))
2434
2435 (when (match-string 3)
2436 (ical:signal-parse-error
2437 (format "Invalid value for `%s' property: %s" type (match-string 3))
2438 :restart-at (1+ limit)))
2439
2440 (let* ((value-begin (match-beginning 2))
2441 (value-end (match-end 2))
2442 (end value-end)
2443 (node (ical:read-property-value type (match-string 2) params)))
2444 (ical:ast-node-meta-set node :buffer (current-buffer))
2445 ;; 'begin must be set by parse-property
2446 (ical:ast-node-meta-set node :value-begin value-begin)
2447 (ical:ast-node-meta-set node :value-end value-end)
2448 (ical:ast-node-meta-set node :end end)
2449
2450 node)))
2451
2452(defun ical:print-property-node (node)
2453 "Serialize a property syntax node NODE to a string."
2454 (setq node (ical:maybe-add-value-param node))
2455 (let* ((type (ical:ast-node-type node))
2456 (list-sep (get type 'ical:list-sep))
2457 (property-name (car (rassq type ical:property-types)))
2458 (name-str (or property-name
2459 (ical:ast-node-meta-get :original-name node)))
2460 (params (ical:ast-node-children node))
2461 (value (ical:ast-node-value node))
2462 (value-str
2463 (or (ical:ast-node-meta-get :original-value node)
2464 ;; any ical:print-error here propagates:
2465 (if list-sep
2466 (string-join (mapcar #'ical:default-value-printer value)
2467 list-sep)
2468 (ical:default-value-printer value)))))
2469
2470 (unless (and (stringp name-str) (length> name-str 0))
2471 (ical:signal-print-error
2472 (format "Unknown property name for type `%s'" type)
2473 :node node))
2474
2475 (concat name-str
2476 (ical:print-params params)
2477 ":"
2478 value-str
2479 "\n")))
2480
2481(defun ical:maybe-add-value-param (property-node)
2482 "Add a VALUE parameter to PROPERTY-NODE if necessary.
2483
2484If the type of PROPERTY-NODE's value is not the same as its
2485default-type, check that its parameter list contains an
2486`icalendar-valuetypeparam' specifying that type as the type for
2487the value. If not, add such a parameter to PROPERTY-NODE's list
2488of parameters. Returns the possibly-modified PROPERTY-NODE.
2489
2490If the parameter list already contains a value type parameter for
2491a type other than the property value's type, an
2492`icalendar-validation-error' is signaled.
2493
2494If PROPERTY's value is a list, the type of the first element will
2495be assumed to be the type for all the values in the list. If the
2496list is empty, no change will be made to PROPERTY's parameters."
2497 (catch 'no-value-type
2498 (let* ((property-type (ical:ast-node-type property-node))
2499 (value/s (ical:ast-node-value property-node))
2500 (value (if (and (ical:expects-list-of-values-p property-type)
2501 (listp value/s))
2502 (car value/s)
2503 value/s))
2504 (value-type (cond ((stringp value) 'ical:text)
2505 ((ical:ast-node-p value)
2506 (ical:ast-node-type value))
2507 ;; if we can't determine a type from the value, bail:
2508 (t (throw 'no-value-type property-node))))
2509 (params (ical:ast-node-children property-node))
2510 (expected-type (ical:value-type-from-params params)))
2511
2512 (when (not (eq value-type (get property-type 'ical:default-type)))
2513 (if expected-type
2514 (when (not (eq value-type expected-type))
2515 (ical:signal-validation-error
2516 (format (concat "Mismatching VALUE parameter. VALUE specifies %s "
2517 "but property value has type %s")
2518 expected-type value-type)
2519 :node property-node))
2520 ;; the value isn't of the default type, but we didn't find a
2521 ;; VALUE parameter, so add one now:
2522 (let* ((valuetype-param
2523 (ical:make-ast-node 'ical:valuetypeparam
2524 (list :value (ical:make-ast-node
2525 'ical:printed-value-type
2526 (list :value value-type)))))
2527 (new-params (cons valuetype-param
2528 (ical:ast-node-children property-node))))
2529 (apply #'ical:ast-node-set-children property-node new-params))))
2530
2531 ;; Return the modified property node:
2532 property-node)))
2533
2534(defun ical:value-type-from-params (params)
2535 "Return the type symbol associated with any VALUE parameter in PARAMS.
2536PARAMS should be a list of parameter nodes. The type symbol specified by
2537the first `icalendar-valuetypeparam' in PARAMS, or nil, will be returned."
2538 (catch 'found
2539 (dolist (param params)
2540 (when (ical:value-param-p param)
2541 (let ((type (ical:ast-node-value
2542 (ical:ast-node-value param))))
2543 (throw 'found type))))))
2544
2545(defun ical:parse-property (limit)
2546 "Parse the current property, up to LIMIT.
2547
2548Point should be at the beginning of a property line; LIMIT should be the
2549position at the end of the line.
2550
2551Returns a syntax node for the property. After parsing, point is at the
2552beginning of the next content line."
2553 (rx-let ((ical:property-start (seq line-start (group-n 1 ical:name))))
2554 (let (line-begin line-end property-name property-type params node)
2555 ;; Property name
2556 (unless (re-search-forward (rx ical:property-start) limit t)
2557 (ical:signal-parse-error
2558 "Malformed property: could not match property name"
2559 :restart-at (1+ limit)))
2560
2561 (setq property-name (match-string 1))
2562 (setq line-begin (line-beginning-position))
2563 (setq line-end (line-end-position))
2564
2565 ;; Parameters
2566 (when (looking-at-p ";")
2567 (setq params (ical:parse-params line-end)))
2568
2569 (unless (looking-at-p ":")
2570 (ical:signal-parse-error
2571 "Malformed property: parameters did not end at colon"
2572 :restart-at (1+ limit)))
2573 (forward-char)
2574
2575 ;; Value
2576 (setq property-type (alist-get (upcase property-name)
2577 ical:property-types
2578 'ical:other-property
2579 nil #'equal))
2580 (setq node (ical:parse-property-value property-type limit params))
2581
2582 ;; sanity check, since e.g. invalid base64 data might not
2583 ;; match all the way to the end of the line, as test
2584 ;; rfc5545-sec3.1.3/2 initially revealed
2585 (unless (eql (point) (line-end-position))
2586 (ical:signal-parse-error
2587 (format "%s property value did not consume line: %s"
2588 property-name
2589 (ical:default-value-printer (ical:ast-node-value node)))
2590 :restart-at (1+ limit)))
2591
2592 ;; value, children are set in ical:read-property-value,
2593 ;; value-begin, value-end, end in ical:parse-property-value.
2594 ;; begin and original-name are only available here:
2595 (ical:ast-node-meta-set node :begin line-begin)
2596 (when (eq property-type 'ical:other-property)
2597 (ical:ast-node-meta-set node :original-name property-name))
2598
2599 ;; Set point up for the next property parser.
2600 (while (not (bolp))
2601 (forward-char))
2602
2603 ;; Return the syntax node
2604 node)))
2605
2606
2607;;;; Section 3.7: Calendar Properties
2608(ical:define-property ical:calscale "CALSCALE"
2609 "Calendar scale.
2610
2611This property specifies the time scale of an
2612`icalendar-vcalendar' object. The only scale defined by RFC5545
2613is \"GREGORIAN\", which is the default."
2614 ;; only allowed value:
2615 "GREGORIAN"
2616 :default "GREGORIAN"
2617 :child-spec (:zero-or-more (ical:otherparam))
2618 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.1")
2619
2620(ical:define-property ical:method "METHOD"
2621 "Method for a scheduling request.
2622
2623When an `icalendar-vcalendar' is sent in a MIME message, this property
2624specifies the semantics of the request in the message: e.g. it is
2625a request to publish the calendar object, or a reply to an
2626invitation. This property and the MIME message's \"method\"
2627parameter value must be the same.
2628
2629RFC5545 does not define any methods, but RFC5546 does; see
2630URL `https://www.rfc-editor.org/rfc/rfc5546.html#section-3.2'"
2631 ;; TODO: implement methods in RFC5546?
2632 ical:iana-token
2633 :child-spec (:zero-or-more (ical:otherparam))
2634 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.2")
2635
2636(ical:define-property ical:prodid "PRODID"
2637 "Product Identifier.
2638
2639This property identifies the program that created an
2640`icalendar-vcalendar' object. It must be specified exactly once in a
2641calendar object. Its value should be a globally unique identifier for
2642the program. RFC5545 suggests using an ISO \"Formal Public Identifier\";
2643see URL `https://en.wikipedia.org/wiki/Formal_Public_Identifier'."
2644 ical:text
2645 :child-spec (:zero-or-more (ical:otherparam))
2646 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.3")
2647
2648(ical:define-property ical:version "VERSION"
2649 "Version (2.0 corresponds to RFC5545).
2650
2651This property specifies the version number of the iCalendar
2652specification to which an `icalendar-vcalendar' object conforms,
2653and must be specified exactly once in a calendar object. It is
2654either the string \"2.0\" or a string like MIN;MAX specifying
2655minimum and maximum versions of future revisions of the
2656specification."
2657 (or "2.0"
2658 ;; minver ";" maxver
2659 (seq ical:iana-token ?\; ical:iana-token))
2660 :child-spec (:zero-or-more (ical:otherparam))
2661 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.4")
2662
2663
2664;;;; Section 3.8:
2665;;;;; Section 3.8.1: Descriptive Component Properties
2666
2667(ical:define-property ical:attach "ATTACH"
2668 "Attachment.
2669
2670This property specifies a file attached to an iCalendar
2671component, either via a URI, or as encoded binary data. In
2672`icalendar-valarm' components, it is used to specify the
2673notification sent by the alarm."
2674 ;; Groups 11, 12 are used in ical:uri
2675 (or (group-n 13 ical:uri)
2676 (group-n 14 ical:binary))
2677 :default-type ical:uri
2678 :other-types (ical:binary)
2679 :child-spec (:zero-or-one (ical:fmttypeparam
2680 ical:valuetypeparam
2681 ical:encodingparam)
2682 :zero-or-more (ical:otherparam))
2683 :other-validator ical:attach-validator
2684 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.1")
2685
2686(defun ical:attach-validator (node)
2687 "Additional validator for an `icalendar-attach' NODE.
2688Checks that NODE has a correct `icalendar-encodingparam' and
2689`icalendar-valuetypeparam' if its value is an `icalendar-binary'.
2690
2691This function is called by `icalendar-ast-node-valid-p' for
2692ATTACH nodes; it is not normally necessary to call it directly."
2693 (let* ((value-node (ical:ast-node-value node))
2694 (value-type (ical:ast-node-type value-node))
2695 (valtypeparam (ical:ast-node-first-child-of 'ical:valuetypeparam node))
2696 (encodingparam (ical:ast-node-first-child-of 'ical:encodingparam node)))
2697
2698 (when (eq value-type 'ical:binary)
2699 (unless (and (ical:ast-node-p valtypeparam)
2700 (eq 'ical:binary
2701 (ical:ast-node-value ; unwrap inner printed-value-type
2702 (ical:ast-node-value valtypeparam))))
2703 (ical:signal-validation-error
2704 "`icalendar-binary' attachment requires 'VALUE=BINARY' parameter"
2705 :node node))
2706 (unless (and (ical:ast-node-p encodingparam)
2707 (equal "BASE64" (ical:ast-node-value encodingparam)))
2708 (ical:signal-validation-error
2709 "`icalendar-binary' attachment requires 'ENCODING=BASE64' parameter"
2710 :node node)))
2711 ;; success:
2712 node))
2713
2714(ical:define-property ical:categories "CATEGORIES"
2715 "Categories.
2716
2717This property lists categories or subtypes of an iCalendar
2718component for e.g. searching or filtering. The categories can be
2719any `icalendar-text' value."
2720 ical:text
2721 :list-sep ","
2722 :child-spec (:zero-or-one (ical:languageparam)
2723 :zero-or-more (ical:otherparam))
2724 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.2")
2725
2726(ical:define-property ical:class "CLASS"
2727 "(Access) Classification.
2728
2729This property specifies the scope of access that the calendar
2730owner intends for a given component, e.g. public or private."
2731 (or "PUBLIC"
2732 "PRIVATE"
2733 "CONFIDENTIAL"
2734 (group-n 5
2735 (or ical:iana-token
2736 ical:x-name)))
2737 ;; "If not specified in a component that allows this property, the
2738 ;; default value is PUBLIC. Applications MUST treat x-name and
2739 ;; iana-token values they don't recognize the same way as they would
2740 ;; the PRIVATE value."
2741 :default "PUBLIC"
2742 :unrecognized "PRIVATE"
2743 :child-spec (:zero-or-more (ical:otherparam))
2744 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.3")
2745
2746(ical:define-property ical:comment "COMMENT"
2747 "Comment to calendar user.
2748
2749This property can be specified multiple times in calendar components,
2750and can contain any `icalendar-text' value."
2751 ical:text
2752 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
2753 :zero-or-more (ical:otherparam))
2754 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.4")
2755
2756(ical:define-property ical:description "DESCRIPTION"
2757 "Description.
2758
2759This property should be a longer, more complete description of
2760the calendar component than is contained in the
2761`icalendar-summary' property. In a `icalendar-vjournal'
2762component, it is used to capture a journal entry, and may be
2763specified multiple times. Otherwise it may only be specified
2764once. In an `icalendar-valarm' component, it contains the
2765notification text for a DISPLAY or EMAIL alarm."
2766 ical:text
2767 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
2768 :zero-or-more (ical:otherparam))
2769 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.5")
2770
2771(defun ical:read-geo-coordinates (s)
2772 "Read an `icalendar-geo-coordinates' value from string S."
2773 (let ((vals (mapcar #'string-to-number (string-split s ";"))))
2774 (cons (car vals) (cadr vals))))
2775
2776(defun ical:print-geo-coordinates (val)
2777 "Serialize an `icalendar-geo-coordinates' value to a string."
2778 (concat (number-to-string (car val)) ";" (number-to-string (cdr val))))
2779
2780(defun ical:geo-coordinates-p (val)
2781 "Return non-nil if VAL is an `icalendar-geo-coordinates' value."
2782 (and (floatp (car val)) (floatp (cdr val))))
2783
2784(ical:define-type ical:geo-coordinates nil ; don't add to ical:value-types
2785 "Type for global positions.
2786
2787This is not a type defined by RFC5545; it is defined here to
2788facilitate parsing the `icalendar-geo' property. When printed, it
2789is represented as a pair of `icalendar-float' values separated by
2790a semicolon, like LATITUDE;LONGITUDE. When read, it is a dotted
2791pair of Elisp floats (LATITUDE . LONGITUDE)."
2792 '(satisfies ical:geo-coordinates-p)
2793 (seq ical:float ";" ical:float)
2794 :reader ical:read-geo-coordinates
2795 :printer ical:print-geo-coordinates)
2796
2797(ical:define-property ical:geo "GEO"
2798 "Global position of a component as a pair LATITUDE;LONGITUDE.
2799
2800Both values are floats representing a number of degrees. The
2801latitude value is north of the equator if positive, and south of
2802the equator if negative. The longitude value is east of the prime
2803meridian if positive, and west of it if negative."
2804 ical:geo-coordinates
2805 :child-spec (:zero-or-more (ical:otherparam))
2806 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.6")
2807
2808(ical:define-property ical:location "LOCATION"
2809 "Location.
2810
2811This property describes the intended location or venue of a
2812component, e.g. a particular room or building, with an
2813`icalendar-text' value. RFC5545 suggests using the
2814`icalendar-altrep' parameter on this property to provide more
2815structured location information."
2816 ical:text
2817 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
2818 :zero-or-more (ical:otherparam))
2819 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7")
2820
2821;; TODO: type for percentages?
2822(ical:define-property ical:percent-complete "PERCENT-COMPLETE"
2823 "Percent Complete.
2824
2825This property describes progress toward the completion of an
2826`icalendar-vtodo' component. It can appear at most once in such a
2827component. If this TODO is assigned to multiple people, the value
2828represents the completion state for each person individually. The
2829value should be between 0 and 100 (though this is not currently
2830enforced here)."
2831 ical:integer
2832 :child-spec (:zero-or-more (ical:otherparam))
2833 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.8")
2834
2835;; TODO: type for priority values?
2836(ical:define-property ical:priority "PRIORITY"
2837 "Priority.
2838
2839This property describes the priority of a component. 0 means an
2840undefined priority. Other values range from 1 (highest priority)
2841to 9 (lowest priority). See RFC5545 for suggestions on how to
2842represent other priority schemes with this property."
2843 ical:integer
2844 :default "0"
2845 :child-spec (:zero-or-more (ical:otherparam))
2846 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.9")
2847
2848(ical:define-property ical:resources "RESOURCES"
2849 "Resources for an activity.
2850
2851This property is a list of `icalendar-text' values that describe
2852any resources required or foreseen for the activity represented
2853by a component, e.g. a projector and screen for a meeting."
2854 ical:text
2855 :list-sep ","
2856 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
2857 :zero-or-more (ical:otherparam))
2858 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.10")
2859
2860(ical:define-type ical:status-keyword nil
2861 "Keyword value of a STATUS property.
2862
2863This is not a real type defined by RFC5545; it is defined here to
2864facilitate parsing that property."
2865 '(and string (satisfies ical:match-status-keyword-value))
2866 ;; Note that this type does NOT allow arbitrary text:
2867 (or "TENTATIVE"
2868 "CONFIRMED"
2869 "CANCELLED"
2870 "NEEDS-ACTION"
2871 "COMPLETED"
2872 "IN-PROCESS"
2873 "DRAFT"
2874 "FINAL"))
2875
2876(ical:define-property ical:status "STATUS"
2877 "Overall status or confirmation.
2878
2879This property is a keyword used by an Organizer to inform
2880Attendees about the status of a component, e.g. whether an
2881`icalendar-vevent' has been cancelled, whether an
2882`icalendar-vtodo' has been completed, or whether an
2883`icalendar-vjournal' is still in draft form. It can be specified
2884at most once on these components."
2885 ical:status-keyword
2886 :child-spec (:zero-or-more (ical:otherparam))
2887 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.11")
2888
2889(ical:define-property ical:summary "SUMMARY"
2890 "Short summary.
2891
2892This property provides a short, one-line description of a
2893component for display purposes. In an EMAIL `icalendar-valarm',
2894it is used as the subject of the email. A longer description of
2895the component can be provided in the `icalendar-description'
2896property."
2897 ical:text
2898 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
2899 :zero-or-more (ical:otherparam))
2900 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.12")
2901
2902;;;;; Section 3.8.2: Date and Time Component Properties
2903
2904(defun ical:property-w/tzid-validator (node)
2905 "Additional validator for property NODE with `icalendar-tzid' parameters.
2906Checks that this parameter does not occur in combination with an
2907`icalendar-date' value or an `icalendar-date-time' in UTC time."
2908 (ical:with-property node
2909 ((ical:tzidparam :first tzidnode))
2910 (when (and tzidnode (eq value-type 'ical:date))
2911 (icalendar-signal-validation-error
2912 "Property cannot contain `icalendar-tzidparam' with `icalendar-date' value"
2913 :node node))
2914 (when (and tzidnode (eq value-type 'ical:date-time)
2915 (ical:date-time-is-utc-p value))
2916 (icalendar-signal-validation-error
2917 "Property cannot contain `icalendar-tzidparam' in combination with UTC time"
2918 :node node))))
2919
2920(ical:define-property ical:completed "COMPLETED"
2921 "Time completed.
2922
2923This property is a timestamp that records the date and time when
2924an `icalendar-vtodo' was actually completed. The value must be an
2925`icalendar-date-time' with a UTC time."
2926 ical:date-time
2927 :child-spec (:zero-or-more (ical:otherparam))
2928 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.1")
2929
2930(ical:define-property ical:dtend "DTEND"
2931 "End time of an event or free/busy block.
2932
2933This property's value specifies when an `icalendar-vevent' or
2934`icalendar-freebusy' ends. Its value must be of the same type as
2935the value of the component's corresponding `icalendar-dtstart'
2936property. The value is a non-inclusive bound, i.e., the value of
2937this property must be the first time or date *after* the end of
2938the event or free/busy block."
2939 (or ical:date-time
2940 ical:date)
2941 :default-type ical:date-time
2942 :other-types (ical:date)
2943 :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
2944 :zero-or-more (ical:otherparam))
2945 :other-validator ical:property-w/tzid-validator
2946 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2")
2947
2948(ical:define-property ical:due "DUE"
2949 "Due date.
2950
2951This property specifies the date (and possibly time) by which an
2952`icalendar-todo' item is expected to be completed, i.e., its
2953deadline. If the component also has an `icalendar-dtstart'
2954property, the two properties must have the same value type, and
2955the value of the DTSTART property must be earlier than the value
2956of this property."
2957 (or ical:date-time
2958 ical:date)
2959 :default-type ical:date-time
2960 :other-types (ical:date)
2961 :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
2962 :zero-or-more (ical:otherparam))
2963 :other-validator ical:property-w/tzid-validator
2964 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.3")
2965
2966(ical:define-property ical:dtstart "DTSTART"
2967 "Start time of a component.
2968
2969This property's value specifies when a component starts. In an
2970`icalendar-vevent', it specifies the start of the event. In an
2971`icalendar-vfreebusy', it specifies the start of the free/busy
2972block. In `icalendar-standard' and `icalendar-daylight'
2973sub-components, it defines the start time of a time zone
2974specification.
2975
2976It is required in any component with an `icalendar-rrule'
2977property, and in any `icalendar-vevent' component contained in a
2978calendar that does not have a `icalendar-method' property.
2979
2980Its value must be of the same type as the value of the
2981component's corresponding `icalendar-dtend' property. In an
2982`icalendar-vtodo' component, it must also be of the same type as
2983the value of an `icalendar-due' property (if present)."
2984 (or ical:date-time
2985 ical:date)
2986 :default-type ical:date-time
2987 :other-types (ical:date)
2988 :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
2989 :zero-or-more (ical:otherparam))
2990 :other-validator ical:property-w/tzid-validator
2991 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4")
2992
2993(ical:define-property ical:duration "DURATION"
2994 "Duration.
2995
2996This property specifies a duration of time for a component.
2997In an `icalendar-vevent', it can be used to implicitly specify
2998the end of the event, instead of an explicit `icalendar-dtend'.
2999In an `icalendar-vtodo', it can likewise be used to implicitly specify
3000the due date, instead of an explicit `icalendar-due'.
3001In an `icalendar-valarm', it used to specify the delay period
3002before the alarm repeats.
3003
3004If a related `icalendar-dtstart' property has an `icalendar-date'
3005value, then the duration must be given as a number of weeks or days."
3006 ical:dur-value
3007 :child-spec (:zero-or-more (ical:otherparam))
3008 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.5")
3009
3010(ical:define-property ical:freebusy "FREEBUSY"
3011 "Free/Busy Times.
3012
3013This property specifies a list of periods of free or busy time in
3014an `icalendar-vfreebusy' component. Whether it specifies free or
3015busy times is determined by its `icalendar-fbtype' parameter. The
3016times in each period must be in UTC format."
3017 ical:period
3018 :list-sep ","
3019 :child-spec (:zero-or-one (ical:fbtypeparam)
3020 :zero-or-more (ical:otherparam))
3021 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.6")
3022
3023(ical:define-property ical:transp "TRANSP"
3024 "Time Transparency for free/busy searches.
3025
3026Note that this property only allows two values: \"TRANSPARENT\"
3027or \"OPAQUE\". An OPAQUE value means that the component consumes
3028time on a calendar. TRANSPARENT means it does not, and thus is
3029invisible to free/busy time searches."
3030 ;; Note that this does NOT allow arbitrary text:
3031 (or "TRANSPARENT"
3032 "OPAQUE")
3033 :default "OPAQUE"
3034 :child-spec (:zero-or-more (ical:otherparam))
3035 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.7")
3036
3037;;;;; Section 3.8.3: Time Zone Component Properties
3038
3039(ical:define-property ical:tzid "TZID"
3040 "Time Zone Identifier.
3041
3042This property specifies the unique identifier for a time zone in
3043an `icalendar-vtimezone' component, and is a required property of
3044that component. This is an identifier that `icalendar-tzidparam'
3045parameters in other components may then refer to."
3046 (seq (zero-or-one "/") ical:text)
3047 :child-spec (:zero-or-more (ical:otherparam))
3048 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.1")
3049
3050(ical:define-property ical:tzname "TZNAME"
3051 "Time Zone Name.
3052
3053This property specifies a customary name for a time zone in
3054`icalendar-daylight' and `icalendar-standard' sub-components."
3055 ical:text
3056 :child-spec (:zero-or-one (ical:languageparam)
3057 :zero-or-more (ical:otherparam))
3058 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.2")
3059
3060(ical:define-property ical:tzoffsetfrom "TZOFFSETFROM"
3061 "Time Zone Offset (prior to observance).
3062
3063This property specifies the time zone offset that is in use
3064*prior to* this time zone observance. It is used to calculate the
3065absolute time at which the observance takes place. It is a
3066required property of an `icalendar-vtimezone' component. Positive
3067numbers indicate time east of the prime meridian (ahead of UTC).
3068Negative numbers indicate time west of the prime meridian (behind
3069UTC)."
3070 ical:utc-offset
3071 :child-spec (:zero-or-more (ical:otherparam))
3072 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.3")
3073
3074(ical:define-property ical:tzoffsetto "TZOFFSETTO"
3075 "Time Zone Offset (in this observance).
3076
3077This property specifies the time zone offset that is in use *in*
3078this time zone observance. It is used to calculate the absolute
3079time at which a new observance takes place. It is a required
3080property of `icalendar-standard' and `icalendar-daylight'
3081components. Positive numbers indicate time east of the prime
3082meridian (ahead of UTC). Negative numbers indicate time west of
3083the prime meridian (behind UTC)."
3084 ical:utc-offset
3085 :child-spec (:zero-or-more (ical:otherparam))
3086 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.4")
3087
3088(ical:define-property ical:tzurl "TZURL"
3089 "Time Zone URL.
3090
3091This property specifies a URL where updated versions of an
3092`icalendar-vtimezone' component are published."
3093 ical:uri
3094 :child-spec (:zero-or-more (ical:otherparam))
3095 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.5")
3096
3097;;;;; Section 3.8.4: Relationship Component Properties
3098
3099(ical:define-property ical:attendee "ATTENDEE"
3100 "Attendee.
3101
3102This property specfies a participant in a `icalendar-vevent',
3103`icalendar-vtodo', or `icalendar-valarm'. It is required when the
3104containing component represents event, task, or notification for
3105a *group* of people, but not for components that simply represent
3106these items in a single user's calendar (in that case, it should
3107not be specified). The property can be specified multiple times,
3108once for each participant in the event or task. In an
3109EMAIL-category VALARM component, this property specifies the
3110address of the user(s) who should receive the notification email.
3111
3112The parameters `icalendar-roleparam', `icalendar-partstatparam',
3113`icalendar-rsvpparam', `icalendar-delfromparam', and
3114`icalendar-deltoparam' are especially relevant for further
3115specifying the roles of each participant in the containing
3116component."
3117 ical:cal-address
3118 :child-spec (:zero-or-one (ical:cutypeparam
3119 ical:memberparam
3120 ical:roleparam
3121 ical:partstatparam
3122 ical:rsvpparam
3123 ical:deltoparam
3124 ical:delfromparam
3125 ical:sentbyparam
3126 ical:cnparam
3127 ical:dirparam
3128 ical:languageparam)
3129 :zero-or-more (ical:otherparam))
3130 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.1")
3131
3132(ical:define-property ical:contact "CONTACT"
3133 "Contact.
3134
3135This property provides textual contact information relevant to an
3136`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
3137`icalendar-vfreebusy'."
3138 ical:text
3139 :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
3140 :zero-or-more (ical:otherparam))
3141 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.2")
3142
3143(ical:define-property ical:organizer "ORGANIZER"
3144 "Organizer.
3145
3146This property specifies the organizer of a group-scheduled
3147`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'.
3148It is required in those components if they represent a calendar
3149entity with multiple participants. In an `icalendar-vfreebusy'
3150component, it used to specify the user requesting free or busy
3151time, or the user who published the calendar that the free/busy
3152information comes from."
3153 ical:cal-address
3154 :child-spec (:zero-or-one (ical:cnparam
3155 ical:dirparam
3156 ical:sentbyparam
3157 ical:languageparam)
3158 :zero-or-more (ical:otherparam))
3159 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.3")
3160
3161(ical:define-property ical:recurrence-id "RECURRENCE-ID"
3162 "Recurrence ID.
3163
3164This property is used together with the `icalendar-uid' and
3165`icalendar-sequence' properties to identify a specific instance
3166of a recurring `icalendar-vevent', `icalendar-vtodo', or
3167`icalendar-vjournal' component. The property value is the
3168original value of the `icalendar-dtstart' property of the
3169recurrence instance. Its value must have the same type as that
3170property's value, and both must specify times in the same way
3171(either local or UTC)."
3172 (or ical:date-time
3173 ical:date)
3174 :default-type ical:date-time
3175 :other-types (ical:date)
3176 :child-spec (:zero-or-one (ical:valuetypeparam
3177 ical:tzidparam
3178 ical:rangeparam)
3179 :zero-or-more (ical:otherparam))
3180 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.4")
3181
3182(ical:define-property ical:related-to "RELATED-TO"
3183 "Related To (component UID).
3184
3185This property specifies the `icalendar-uid' value of a different,
3186related calendar component. It can be specified on an
3187`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'
3188component. An `icalendar-reltypeparam' can be used to specify the
3189relationship type."
3190 ical:text
3191 :child-spec (:zero-or-one (ical:reltypeparam)
3192 :zero-or-more (ical:otherparam))
3193 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.5")
3194
3195(ical:define-property ical:url "URL"
3196 "Uniform Resource Locator.
3197
3198This property specifies the URL associated with an
3199`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
3200`icalendar-vfreebusy' component."
3201 ical:uri
3202 :child-spec (:zero-or-more (ical:otherparam))
3203 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.6")
3204
3205;; TODO: UID should probably be its own type
3206(ical:define-property ical:uid "UID"
3207 "Unique Identifier.
3208
3209This property specifies a globally unique identifier for the
3210containing component, and is required in an `icalendar-vevent',
3211`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vfreebusy'
3212component.
3213
3214RFC5545 requires that the program generating the UID guarantee
3215that it be unique, and recommends generating it in a format which
3216includes a timestamp on the left hand side of an '@' character,
3217and the domain name or IP address of the host on the right-hand
3218side."
3219 ical:text
3220 :child-spec (:zero-or-more (ical:otherparam))
3221 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.7")
3222
3223;;;;; Section 3.8.5: Recurrence Component Properties
3224
3225(ical:define-property ical:exdate "EXDATE"
3226 "Exception Date-Times.
3227
3228This property defines a list of exceptions to a recurrence rule
3229in an `icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
3230`icalendar-standard', or `icalendar-daylight' component. Together
3231with the `icalendar-dtstart', `icalendar-rrule', and
3232`icalendar-rdate' properties, it defines the recurrence set of
3233the component."
3234 (or ical:date-time
3235 ical:date)
3236 :default-type ical:date-time
3237 :other-types (ical:date)
3238 :list-sep ","
3239 :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
3240 :zero-or-more (ical:otherparam))
3241 :other-validator ical:property-w/tzid-validator
3242 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.1")
3243
3244(ical:define-property ical:rdate "RDATE"
3245 "Recurrence Date-Times.
3246
3247This property defines a list of date-times or dates on which an
3248`icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
3249`icalendar-standard', or `icalendar-daylight' component recurs.
3250Together with the `icalendar-dtstart', `icalendar-rrule', and
3251`icalendar-exdate' properties, it defines the recurrence set of
3252the component."
3253 (or ical:period
3254 ical:date-time
3255 ical:date)
3256 :default-type ical:date-time
3257 :other-types (ical:date ical:period)
3258 :list-sep ","
3259 :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
3260 :zero-or-more (ical:otherparam))
3261 :other-validator ical:property-w/tzid-validator
3262 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.2")
3263
3264(ical:define-property ical:rrule "RRULE"
3265 "Recurrence Rule.
3266
3267This property defines a rule or repeating pattern for the dates
3268and times on which an `icalendar-vevent', `icalendar-todo',
3269`icalendar-vjournal', `icalendar-standard', or
3270`icalendar-daylight' component recurs. Together with the
3271`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate'
3272properties, it defines the recurrence set of the component."
3273 ical:recur
3274 ;; TODO: faces for subexpressions?
3275 :child-spec (:zero-or-more (ical:otherparam))
3276 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.3")
3277
3278;;;;; Section 3.8.6: Alarm Component Properties
3279
3280(ical:define-property ical:action "ACTION"
3281 "Action (when alarm triggered).
3282
3283This property defines the action to be taken when the containing
3284`icalendar-valarm' component is triggered. It is a required
3285property in an alarm component."
3286 (or "AUDIO"
3287 "DISPLAY"
3288 "EMAIL"
3289 (group-n 5
3290 (or ical:iana-token
3291 ical:x-name)))
3292 ;; "Applications MUST ignore alarms with x-name and iana-token values
3293 ;; they don't recognize." This substitute is not defined in the
3294 ;; standard but is the simplest way to parse such alarms:
3295 :unrecognized "IGNORE"
3296 :default-type ical:text
3297 :child-spec (:zero-or-more (ical:otherparam))
3298 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.1")
3299
3300(ical:define-property ical:repeat "REPEAT"
3301 "Repeat Count (after initial trigger).
3302
3303This property specifies the number of times an `icalendar-valarm'
3304should repeat after it is initially triggered. This property,
3305along with the `icalendar-duration' property, is required if the
3306alarm triggers more than once."
3307 ical:integer
3308 :default "0"
3309 :child-spec (:zero-or-more (ical:otherparam))
3310 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.2")
3311
3312(ical:define-property ical:trigger "TRIGGER"
3313 "Trigger.
3314
3315This property specifies when an `icalendar-valarm' should
3316trigger. If the value is an `icalendar-dur-value', it represents
3317a time of that duration relative to the start or end of a related
3318`icalendar-vevent' or `icalendar-vtodo'. Whether the trigger
3319applies to the start time or end time of the related component
3320can be specified with the `icalendar-trigrelparam' parameter. A
3321positive duration value triggers after the start or end of the
3322related component; a negative duration value triggers before.
3323
3324If the value is an `icalendar-date-time', it must be in UTC
3325format, and it triggers at the specified time."
3326 (or ical:dur-value
3327 ical:date-time)
3328 :default-type ical:dur-value
3329 :other-types (ical:date-time)
3330 :child-spec (:zero-or-one (ical:valuetypeparam ical:trigrelparam)
3331 :zero-or-more (ical:otherparam))
3332 :other-validator ical:trigger-validator
3333 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.3")
3334
3335(defun ical:trigger-validator (node)
3336 "Additional validator for an `icalendar-trigger' NODE.
3337Checks that NODE has valid parameters depending on the type of its value.
3338
3339This function is called by `icalendar-ast-node-valid-p' for
3340TRIGGER nodes; it is not normally necessary to call it directly."
3341 (let* ((params (ical:ast-node-children node))
3342 (value-node (ical:ast-node-value node))
3343 (value-type (and value-node (ical:ast-node-type value-node))))
3344 (when (eq value-type 'ical:date-time)
3345 (let ((expl-type (ical:value-type-from-params params))
3346 (dt-value (ical:ast-node-value value-node)))
3347 (unless (eq expl-type 'ical:date-time)
3348 (ical:signal-validation-error
3349 (concat "Explicit `icalendar-valuetypeparam' required in "
3350 "`icalendar-trigger' with non-duration value")
3351 :node node))
3352 (when (ical:ast-node-first-child-of 'ical:trigrelparam node)
3353 (ical:signal-validation-error
3354 (concat "`icalendar-trigrelparam' not allowed in "
3355 "`icalendar-trigger' with non-duration value")
3356 :node node))
3357 (unless (ical:date-time-is-utc-p dt-value)
3358 (ical:signal-validation-error
3359 (concat "`icalendar-date-time' value of `icalendar-trigger' "
3360 "must be in UTC time")
3361 :node node))))
3362 ;; success:
3363 node))
3364
3365;;;;; Section 3.8.7: Change Management Component Properties
3366
3367(ical:define-property ical:created "CREATED"
3368 "Date-Time Created.
3369
3370This property specifies the date and time when the calendar user
3371initially created an `icalendar-vevent', `icalendar-vtodo', or
3372`icalendar-vjournal' in the calendar database. The value must be
3373in UTC time."
3374 ical:date-time
3375 :child-spec (:zero-or-more (ical:otherparam))
3376 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1")
3377
3378(ical:define-property ical:dtstamp "DTSTAMP"
3379 "Timestamp (of last revision or instance creation).
3380
3381In an `icalendar-vevent', `icalendar-vtodo',
3382`icalendar-vjournal', or `icalendar-vfreebusy', this property
3383specifies the date and time when the calendar user last revised
3384the component's data in the calendar database. (In this case, it
3385is equivalent to the `icalendar-last-modified' property.)
3386
3387If this property is specified on an `icalendar-vcalendar' object
3388which contains an `icalendar-method' property, it specifies the
3389date and time when that instance of the calendar object was
3390created. In this case, it differs from the `icalendar-creation'
3391and `icalendar-last-modified' properties: whereas those specify
3392the time the underlying data was created and last modified in the
3393calendar database, this property specifies when the calendar
3394object *representing* that data was created.
3395
3396The value must be in UTC time."
3397 ical:date-time
3398 :child-spec (:zero-or-more (ical:otherparam))
3399 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.2")
3400
3401(ical:define-property ical:last-modified "LAST-MODIFIED"
3402 "Last Modified timestamp.
3403
3404This property specifies when the data in an `icalendar-vevent',
3405`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vtimezone'
3406was last modified in the calendar database."
3407 ical:date-time
3408 :child-spec (:zero-or-more (ical:otherparam))
3409 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.3")
3410
3411(ical:define-property ical:sequence "SEQUENCE"
3412 "Revision Sequence Number.
3413
3414This property specifies the number of the current revision in a
3415sequence of revisions in an `icalendar-vevent',
3416`icalendar-vtodo', or `icalendar-vjournal' component. It starts
3417at 0 and should be incremented monotonically every time the
3418Organizer makes a significant revision to the calendar data that
3419component represents."
3420 ical:integer
3421 :default "0"
3422 :child-spec (:zero-or-more (ical:otherparam))
3423 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.4")
3424
3425;;;;; Section 3.8.8: Miscellaneous Component Properties
3426;; IANA and X- properties should be parsed and printed but can be ignored:
3427(ical:define-property ical:other-property nil ; don't add to ical:property-types
3428 "IANA or X-name property.
3429
3430This property type corresponds to the IANA Properties and
3431Non-Standard Properties defined in RFC5545; it represents
3432properties with an unknown name (matching rx
3433`icalendar-iana-token' or `icalendar-x-name') whose values must
3434be parsed and preserved but not further interpreted. Its value
3435may be set to any type with the `icalendar-valuetypeparam'
3436parameter."
3437 ical:value
3438 :default-type ical:text
3439 ;; "The default value type is TEXT. The value type can be set to any
3440 ;; value type." TODO: should we specify :other-types? Without it, a
3441 ;; VALUE param will be required to parse anything other than text,
3442 ;; but that seems reasonable.
3443 :child-spec (:allow-others t)
3444 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8")
3445
3446(defun ical:read-req-status-info (s)
3447 "Read a request status value from S.
3448S should have been previously matched against `icalendar-request-status-info'."
3449 ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
3450 (ignore s)
3451 (let ((code (match-string 11))
3452 (desc (match-string 12))
3453 (exdata (match-string 13)))
3454 (list code (ical:read-text desc) (when exdata (ical:read-text exdata)))))
3455
3456(defun ical:print-req-status-info (rsi)
3457 "Serialize request status info value RSI to a string."
3458 (let ((code (car rsi))
3459 (desc (cadr rsi))
3460 (exdata (caddr rsi)))
3461 (if exdata
3462 (format "%s;%s;%s" code (ical:print-text desc) (ical:print-text exdata))
3463 (format "%s;%s" code (ical:print-text desc)))))
3464
3465(defun ical:req-status-info-p (val)
3466 "Return non-nil if VAL is an `icalendar-request-status-info' value."
3467 (and (listp val)
3468 (length= val 3)
3469 (stringp (car val))
3470 (stringp (cadr val))
3471 (cl-typep (caddr val) '(or string null))))
3472
3473(ical:define-type ical:req-status-info nil
3474 "Type for REQUEST-STATUS property values.
3475
3476When read, a list (CODE DESCRIPTION EXCEPTION). CODE is a hierarchical
3477numerical code, represented as a string, with the following meanings:
3478 1.xx Preliminary success
3479 2.xx Successful
3480 3.xx Client Error
3481 4.xx Scheduling Error
3482DESCRIPTION is a longer description of the request status, also a string.
3483EXCEPTION (which may be nil) is textual data describing an error.
3484
3485When printed, the three elements are separated by semicolons, like
3486 CODE;DESCRIPTION;EXCEPTION
3487or
3488 CODE;DESCRIPTION
3489if EXCEPTION is nil.
3490
3491This is not a type defined by RFC5545; it is defined here to
3492facilitate parsing the `icalendar-request-status' property."
3493 '(satisfies ical:req-status-info-p)
3494 (seq
3495 ;; statcode: hierarchical status code
3496 (group-n 11
3497 (seq (one-or-more digit)
3498 (** 1 2 (seq ?. (one-or-more digit)))))
3499 ?\;
3500 ;; statdesc: status description
3501 (group-n 12 ical:text)
3502 ;; exdata: exception data
3503 (zero-or-one (seq ?\; (group-n 13 ical:text))))
3504 :reader ical:read-req-status-info
3505 :printer ical:print-req-status-info
3506 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")
3507
3508(ical:define-property ical:request-status "REQUEST-STATUS"
3509 "Request status"
3510 ical:req-status-info
3511 :child-spec (:zero-or-one (ical:languageparam)
3512 :zero-or-more (ical:otherparam))
3513 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")
3514
3515
3516;;; Section 3.6: Calendar Components
3517
3518(defvar ical:component-types nil ;; populated by ical:define-component
3519 "Alist mapping printed component names to type symbols.")
3520
3521(defun ical:parse-component (limit)
3522 "Parse an iCalendar component from point up to LIMIT.
3523Point should be at the start of the component, i.e., at the start
3524of a line that looks like \"BEGIN:[COMPONENT-NAME]\". After parsing,
3525point is at the beginning of the next line following the component
3526\(or end of the buffer). Returns a syntax node representing the component."
3527 (let ((begin-pos nil)
3528 (body-begin-pos nil)
3529 (end-pos nil)
3530 (body-end-pos nil)
3531 (begin-regex (rx line-start "BEGIN:" (group-n 2 ical:name) line-end)))
3532
3533 (unless (re-search-forward begin-regex limit t)
3534 (ical:signal-parse-error "Not at start of a component"))
3535
3536 (setq begin-pos (match-beginning 0)
3537 body-begin-pos (1+ (match-end 0))) ; start of next line
3538
3539 (let* ((component-name (match-string 2))
3540 (known-type (alist-get (upcase component-name)
3541 ical:component-types
3542 nil nil #'equal))
3543 (component-type (or known-type 'ical:other-component))
3544 child children)
3545
3546 ;; Find end of component:
3547 (save-excursion
3548 (if (re-search-forward (concat "^END:" component-name "$") limit t)
3549 (setq end-pos (match-end 0)
3550 body-end-pos (1- (match-beginning 0))) ; end of prev. line
3551 (ical:signal-parse-error
3552 (format "Matching 'END:%s' not found between %d and %d"
3553 component-name begin-pos limit)
3554 :restart-at (1+ limit))))
3555
3556 (while (not (bolp))
3557 (forward-char))
3558
3559 ;; Parse the properties and subcomponents of this component:
3560 (while (<= (point) body-end-pos)
3561 (condition-case err
3562 (setq child (ical:parse-property-or-component end-pos))
3563 (ical:parse-error
3564 (ical:handle-parse-error err)
3565 (setq child nil)))
3566 (when child (push child children)))
3567
3568 ;; Set point up for the next parser:
3569 (goto-char end-pos)
3570 (while (and (< (point) (point-max)) (not (bolp)))
3571 (forward-char))
3572
3573 ;; Return the syntax node for the component:
3574 (when children
3575 (ical:make-ast-node component-type
3576 (list
3577 :original-name
3578 (when (eq component-type 'ical:other-component)
3579 component-name)
3580 :buffer (current-buffer)
3581 :begin begin-pos
3582 :end end-pos
3583 :value-begin body-begin-pos
3584 :value-end body-end-pos)
3585 (nreverse children))))))
3586
3587(defun ical:parse-property-or-component (limit)
3588 "Parse a component or a property at point, up to LIMIT.
3589Point should be at the beginning of a line which begins a
3590component or contains a property."
3591 (cond ((looking-at-p (rx line-start "BEGIN:" ical:name line-end))
3592 (ical:parse-component limit))
3593 ((looking-at-p (rx line-start ical:name))
3594 (ical:parse-property (line-end-position)))
3595 (t (ical:signal-parse-error
3596 "Not at start of property or component"
3597 :restart-at ; find start of next content line:
3598 (save-excursion
3599 (if (re-search-forward (rx line-start ical:name) nil t)
3600 (match-beginning 0)
3601 (point-max)))))))
3602
3603(defun ical:print-component-node (node)
3604 "Serialize a component syntax node NODE to a string."
3605 (let* ((type (ical:ast-node-type node))
3606 (name (or (ical:ast-node-meta-get :original-name node)
3607 (car (rassq type ical:component-types))))
3608 (children (ical:ast-node-children node))
3609 body)
3610
3611 (unless name
3612 (ical:signal-print-error
3613 (format "Unknown component name for type `%s'" type)
3614 :node node))
3615
3616 (dolist (child children)
3617 (condition-case err
3618 (setq body
3619 (concat body (ical:print-property-or-component child)))
3620 (ical:print-error
3621 (if (ical:ast-node-required-child-p child node)
3622 (ical:signal-print-error
3623 (format
3624 "Unable to print required `%s' %s in `%s' component. Error was:\n%s"
3625 (ical:ast-node-type child)
3626 (if (ical:component-node-p child) "subcomponent" "property")
3627 (ical:ast-node-type node)
3628 (plist-get (cdr err) :message))
3629 :node node)
3630 (ical:handle-print-error err)))))
3631 (concat
3632 (format "BEGIN:%s\n" name)
3633 body
3634 (format "END:%s\n" name))))
3635
3636(defun ical:print-property-or-component (node)
3637 "Serialize a property or component node NODE to a string."
3638 (cond ((ical:property-node-p node)
3639 (ical:print-property-node node))
3640 ((ical:component-node-p node)
3641 (ical:print-component-node node))
3642 (t (ical:signal-print-error "Not a component or property node"
3643 :node node))))
3644
3645(ical:define-component ical:vevent "VEVENT"
3646 "Represents an event.
3647
3648This component contains properties which describe an event, such
3649as its start and end time (`icalendar-dtstart' and
3650`icalendar-dtend') and a summary (`icalendar-summary') and
3651description (`icalendar-description'). It may also contain
3652`icalendar-valarm' components as subcomponents which describe
3653reminder notifications related to the event. Event components can
3654only be direct children of an `icalendar-vcalendar'; they cannot
3655be subcomponents of any other component."
3656 :child-spec (:one (ical:dtstamp ical:uid)
3657 :zero-or-one (ical:dtstart
3658 ;; TODO: dtstart required if METHOD not present
3659 ;; in parent calendar
3660 ical:class
3661 ical:created
3662 ical:description
3663 ical:dtend
3664 ical:duration
3665 ical:geo
3666 ical:last-modified
3667 ical:location
3668 ical:organizer
3669 ical:priority
3670 ical:sequence
3671 ical:status
3672 ical:summary
3673 ical:transp
3674 ical:url
3675 ical:recurrence-id
3676 ical:rrule)
3677 :zero-or-more (ical:attach
3678 ical:attendee
3679 ical:categories
3680 ical:comment
3681 ical:contact
3682 ical:exdate
3683 ical:request-status
3684 ical:related-to
3685 ical:resources
3686 ical:rdate
3687 ical:other-property
3688 ical:valarm))
3689 :other-validator ical:vevent-validator
3690 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1")
3691
3692(defun ical:rrule-validator (node)
3693 "Validate that NODE has the properties required by a recurrence rule.
3694
3695NODE should represent an iCalendar component. When NODE has an
3696`icalendar-rrule' property, this function validates that its
3697`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate'
3698properties satisfy the requirements imposed by this rule.
3699
3700This function is called by the additional validator functions for
3701component nodes (e.g. `icalendar-vevent-validator'); it is not normally
3702necessary to call it directly."
3703 (let* ((rrule (ical:ast-node-first-child-of 'ical:rrule node))
3704 (recval (when rrule (ical:ast-node-value rrule)))
3705 (dtstart (ical:ast-node-first-child-of 'ical:dtstart node))
3706 (start (when dtstart (ical:ast-node-value dtstart)))
3707 (rdates (ical:ast-node-children-of 'ical:rdate node))
3708 (included (when rdates
3709 (mapcar #'ical:ast-node-value
3710 (apply #'append
3711 (mapcar #'ical:ast-node-value rdates))))))
3712 (when rrule
3713 (unless dtstart
3714 (ical:signal-validation-error
3715 "An `icalendar-rrule' requires an `icalendar-dtstart' property"
3716 :node node))
3717 (when included
3718 ;; ""RDATE" in this usage [i.e., in STANDARD and DAYLIGHT
3719 ;; subcomponents] MUST be specified as a date with local time
3720 ;; value, relative to the UTC offset specified in the
3721 ;; "TZOFFSETFROM" property."
3722 (when (and (memq (ical:ast-node-type node) '(ical:standard ical:daylight)))
3723 (unless (ical:list-of-p included 'ical:date-time)
3724 (ical:signal-validation-error
3725 (format
3726 (concat "`icalendar-rdate' values must be `icalendar-date-time' "
3727 "values in %s components")
3728 (ical:ast-node-type node))
3729 :node node))
3730 (when (seq-some #'decoded-time-zone included)
3731 (ical:signal-validation-error
3732 (format
3733 (concat "`icalendar-rdate' values must be in local (\"floating\")"
3734 "time in %s components")
3735 (ical:ast-node-type node))
3736 :node node))))
3737
3738 (let* ((freq (car (alist-get 'FREQ recval)))
3739 (until (car (alist-get 'UNTIL recval))))
3740 (when (eq 'ical:date (ical:ast-node-type start))
3741 (when (or (memq freq '(HOURLY MINUTELY SECONDLY))
3742 (assq 'BYSECOND recval)
3743 (assq 'BYMINUTE recval)
3744 (assq 'BYHOUR recval))
3745 (ical:signal-validation-error
3746 (concat "`icalendar-rrule' must not contain time-based "
3747 "rules when `icalendar-dtstart' is a plain date")
3748 :node node)))
3749 (when until
3750 (unless (eq (ical:ast-node-type start)
3751 (ical:ast-node-type until))
3752 (ical:signal-validation-error
3753 (concat "`icalendar-rrule' UNTIL clause must agree with "
3754 "type of `icalendar-dtstart' property")
3755 :node node))
3756 (when (eq 'ical:date-time (ical:ast-node-type until))
3757 (let ((until-zone
3758 (decoded-time-zone (ical:ast-node-value until)))
3759 (start-zone
3760 (decoded-time-zone (ical:ast-node-value start))))
3761 ;; "If the "DTSTART" property is specified as a date
3762 ;; with local time, then the UNTIL rule part MUST also
3763 ;; be specified as a date with local time":
3764 (when (and (null start-zone) (not (null until-zone)))
3765 (ical:signal-validation-error
3766 (concat "`icalendar-rrule' UNTIL clause must be in "
3767 "local time if `icalendar-dtstart' is")
3768 :node node))
3769 ;; "If the "DTSTART" property is specified as a date
3770 ;; with UTC time or a date with local time and time zone
3771 ;; reference, then the UNTIL rule part MUST be specified
3772 ;; as a date with UTC time":
3773 (when (and (integerp start-zone)
3774 (not (ical:date-time-is-utc-p until)))
3775 (ical:signal-validation-error
3776 (concat "`icalendar-rrule' UNTIL clause must be in UTC time "
3777 "if `icalendar-dtstart' has a defined time zone")
3778 :node node))))
3779 (when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
3780 ;; "In the case of the "STANDARD" and "DAYLIGHT"
3781 ;; sub-components the UNTIL rule part MUST always be
3782 ;; specified as a date with UTC time":
3783 (unless (ical:date-time-is-utc-p until)
3784 (ical:signal-validation-error
3785 (concat "`icalendar-rrule' UNTIL clause must be in UTC time in "
3786 "`icalendar-standard' and `icalendar-daylight' components")
3787 :node node))))
3788
3789 ;; "DTSTART in this usage [i.e., in STANDARD and DAYLIGHT
3790 ;; subcomponents] MUST be specified as a date with a local
3791 ;; time value."
3792 (when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
3793 (unless (eq 'ical:date-time (ical:ast-node-type start))
3794 (ical:signal-validation-error
3795 (concat "`icalendar-dtstart' must be an `icalendar-date-time' in "
3796 "`icalendar-standard' and `icalendar-daylight' components")
3797 :node node))
3798
3799 (when (decoded-time-zone (ical:ast-node-value start))
3800 (ical:signal-validation-error
3801 (concat "`icalendar-dtstart' must be in local (\"floating\") time in "
3802 "`icalendar-standard' and `icalendar-daylight' components")
3803 :node node)))))
3804
3805 ;; Success:
3806 node))
3807
3808(defun ical:vevent-validator (node)
3809 "Additional validator for an `icalendar-vevent' NODE.
3810Checks that NODE has does not have both `icalendar-duration' and
3811`icalendar-dtend' properties, and calls `icalendar-rrule-validator'.
3812
3813This function is called by `icalendar-ast-node-valid-p' for
3814VEVENT nodes; it is not normally necessary to call it directly."
3815 (let* ((duration (ical:ast-node-first-child-of 'ical:duration node))
3816 (dur-value (when duration (ical:ast-node-value
3817 (ical:ast-node-value duration))))
3818 (dtend (ical:ast-node-first-child-of 'ical:dtend node))
3819 (dtstart (ical:ast-node-first-child-of 'ical:dtstart node)))
3820 (when (and dtend duration)
3821 (ical:signal-validation-error
3822 (concat "`icalendar-dtend' and `icalendar-duration' properties must "
3823 "not appear in the same `icalendar-vevent'")
3824 :node node))
3825 ;; don't allow time-based durations with dates
3826 ;; TODO: check that the standard disallows this...?
3827 (when (and dtstart duration
3828 (eq 'ical:date (ical:ast-node-type dtstart))
3829 (or (not (integerp dur-value))
3830 (decoded-time-hour dur-value)
3831 (decoded-time-minute dur-value)
3832 (decoded-time-second dur-value)))
3833 (ical:signal-validation-error
3834 (concat "Event with `icalendar-date' value in `icalendar-dtstart' "
3835 "cannot have time units in `icalendar-duration'")
3836 :node node))
3837
3838 (ical:rrule-validator node)
3839 ;; success:
3840 node))
3841
3842(ical:define-component ical:vtodo "VTODO"
3843 "Represents a To-Do item or task.
3844
3845This component contains properties which describe a to-do item or
3846task, such as its due date (`icalendar-due') and a summary
3847(`icalendar-summary') and description (`icalendar-description').
3848It may also contain `icalendar-valarm' components as
3849subcomponents which describe reminder notifications related to
3850the task. To-do components can only be direct children of an
3851`icalendar-vcalendar'; they cannot be subcomponents of any other
3852component."
3853 :child-spec (:one (ical:dtstamp ical:uid)
3854 :zero-or-one (ical:class
3855 ical:completed
3856 ical:created
3857 ical:description
3858 ical:dtstart
3859 ical:due
3860 ical:duration
3861 ical:geo
3862 ical:last-modified
3863 ical:location
3864 ical:organizer
3865 ical:percent-complete
3866 ical:priority
3867 ical:recurrence-id
3868 ical:sequence
3869 ical:status
3870 ical:summary
3871 ical:url
3872 ical:rrule)
3873 :zero-or-more (ical:attach
3874 ical:attendee
3875 ical:categories
3876 ical:comment
3877 ical:contact
3878 ical:exdate
3879 ical:request-status
3880 ical:related-to
3881 ical:resources
3882 ical:rdate
3883 ical:other-property
3884 ical:valarm))
3885 :other-validator ical:vtodo-validator
3886 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.2")
3887
3888(defun ical:vtodo-validator (node)
3889 "Additional validator for an `icalendar-vtodo' NODE.
3890Checks that NODE has conformant `icalendar-due',
3891`icalendar-duration', and `icalendar-dtstart' properties, and calls
3892`icalendar-rrule-validator'.
3893
3894This function is called by `icalendar-ast-node-valid-p' for
3895VTODO nodes; it is not normally necessary to call it directly."
3896 (let* ((due (ical:ast-node-first-child-of 'ical:due node))
3897 (duration (ical:ast-node-first-child-of 'ical:duration node))
3898 (dtstart (ical:ast-node-first-child-of 'ical:dtstart node)))
3899 (when (and due duration)
3900 (ical:signal-validation-error
3901 (concat "`icalendar-due' and `icalendar-duration' properties "
3902 "must not appear in the same `icalendar-vtodo'")
3903 :node node))
3904 (when (and duration (not dtstart))
3905 (ical:signal-validation-error
3906 (concat "`icalendar-duration' requires `icalendar-dtstart' "
3907 "property in the same `icalendar-vtodo'")
3908 :node node)))
3909 (ical:rrule-validator node)
3910 ;; success:
3911 node)
3912
3913(ical:define-component ical:vjournal "VJOURNAL"
3914 "Represents a journal entry.
3915
3916This component contains properties which describe a journal
3917entry, which might be any longer-form data (e.g., meeting notes,
3918a diary entry, or information needed to complete a task). It can
3919be associated with an `icalendar-vevent' or `icalendar-vtodo' via
3920the `icalendar-related-to' property. A journal entry does not
3921take up time in a calendar, and plays no role in searches for
3922free or busy time. Journal components can only be direct children
3923of `icalendar-vcalendar'; they cannot be subcomponents of any
3924other component."
3925 :child-spec (:one (ical:dtstamp ical:uid)
3926 :zero-or-one (ical:class
3927 ical:created
3928 ical:dtstart
3929 ical:last-modified
3930 ical:organizer
3931 ical:recurrence-id
3932 ical:sequence
3933 ical:status
3934 ical:summary
3935 ical:url
3936 ical:rrule)
3937 :zero-or-more (ical:attach
3938 ical:attendee
3939 ical:categories
3940 ical:comment
3941 ical:contact
3942 ical:description
3943 ical:exdate
3944 ical:related-to
3945 ical:rdate
3946 ical:request-status
3947 ical:other-property)
3948 :other-validator ical:rrule-validator)
3949 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.3")
3950
3951(ical:define-component ical:vfreebusy "VFREEBUSY"
3952 "Represents a published set of free/busy time blocks, or a request
3953or response for such blocks.
3954
3955The free/busy information is represented by the
3956`icalendar-freebusy' property (which may be given more than once)
3957and the related `icalendar-fbtype' parameter. Note that
3958recurrence properties (`icalendar-rrule', `icalendar-rdate', and
3959`icalendar-exdate') are NOT permitted in this component.
3960
3961When used to publish blocks of free/busy time in a user's
3962schedule, the `icalendar-organizer' property specifies the user.
3963
3964When used to request free/busy time in a user's schedule, or to
3965respond to such a request, the `icalendar-attendee' property
3966specifies the user whose time is being requested, and the
3967`icalendar-organizer' property specifies the user making the
3968request.
3969
3970Free/busy components can only be direct children
3971of `icalendar-vcalendar'; they cannot be subcomponents of any
3972other component, and cannot contain subcomponents."
3973 :child-spec (:one (ical:dtstamp ical:uid)
3974 :zero-or-one (ical:contact
3975 ical:dtstart
3976 ical:dtend
3977 ical:organizer
3978 ical:url)
3979 :zero-or-more (ical:attendee
3980 ical:comment
3981 ical:freebusy
3982 ical:request-status
3983 ical:other-property))
3984 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.4")
3985
3986;; TODO: RFC7808 defines additional properties that are relevant here:
3987;; https://www.rfc-editor.org/rfc/rfc7808.html#section-7
3988(ical:define-component ical:vtimezone "VTIMEZONE"
3989 "Represents a time zone.
3990
3991A time zone is identified by an `icalendar-tzid' property, which
3992is required in this component. Times in other calendar components
3993can be specified in local time in this time zone with the
3994`icalendar-tzidparam' parameter. An `icalendar-vcalendar' object
3995must contain exactly one `icalendar-vtimezone' component for each
3996unique time zone identifier used in the calendar.
3997
3998Besides the time zone identifier, a time zone component must
3999contain at least one `icalendar-standard' or `icalendar-daylight'
4000subcomponent, which describe the observance of standard or
4001daylight time in the time zone, including the dates of the
4002observance and the relevant offsets from UTC time."
4003 :child-spec (:one (ical:tzid)
4004 :zero-or-one (ical:last-modified
4005 ical:tzurl)
4006 :zero-or-more (ical:standard
4007 ical:daylight
4008 ical:other-property))
4009 :other-validator ical:vtimezone-validator
4010 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
4011
4012(defun ical:vtimezone-validator (node)
4013 "Additional validator for an `icalendar-vtimezone' NODE.
4014Checks that NODE has at least one `icalendar-standard' or
4015`icalendar-daylight' child.
4016
4017This function is called by `icalendar-ast-node-valid-p' for
4018VTIMEZONE nodes; it is not normally necessary to call it directly."
4019 (let ((child-counts (ical:count-children-by-type node)))
4020 (when (and (= 0 (alist-get 'ical:standard child-counts 0))
4021 (= 0 (alist-get 'ical:daylight child-counts 0)))
4022 (ical:signal-validation-error
4023 (concat "`icalendar-vtimezone' must have at least one "
4024 "`icalendar-standard' or `icalendar-daylight' child")
4025 :node node)))
4026
4027 ;; success:
4028 node)
4029
4030(ical:define-component ical:standard "STANDARD"
4031 "Represents a Standard Time observance in a time zone.
4032
4033The observance has a start time, specified by an
4034`icalendar-dtstart' property, which is required in this component
4035and must be in *local* time format. The observance may have a
4036recurring onset (e.g. each year on a particular day or date)
4037described by the `icalendar-rrule' and `icalendar-rdate'
4038properties. An end date for the observance, if there is one, must
4039be specified in the UNTIL clause of the `icalendar-rrule' in UTC
4040time.
4041
4042The offset from UTC time when the observance begins is specified
4043in the `icalendar-tzoffsetfrom' property, which is required. The
4044offset from UTC time while the observance is in effect is
4045specified by the `icalendar-tzoffsetto' property, which is also
4046required. A common identifier for the time zone observance can be
4047specified in the `icalendar-tzname' property. Other explanatory
4048comments can be provided in `icalendar-comment'.
4049
4050This component must be a direct child of an `icalendar-vtimezone'
4051component and cannot contain other subcomponents."
4052 :child-spec (:one (ical:dtstart
4053 ical:tzoffsetto
4054 ical:tzoffsetfrom)
4055 :zero-or-one (ical:rrule)
4056 :zero-or-more (ical:comment
4057 ical:rdate
4058 ical:tzname
4059 ical:other-property)
4060 :other-validator ical:rrule-validator)
4061 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
4062
4063(ical:define-component ical:daylight "DAYLIGHT"
4064 "Represents a Daylight Savings Time observance in a time zone.
4065
4066The observance has a start time, specified by an
4067`icalendar-dtstart' property, which is required in this component
4068and must be in *local* time format. The observance may have a
4069recurring onset (e.g. each year on a particular day or date)
4070described by the `icalendar-rrule' and `icalendar-rdate'
4071properties. An end date for the observance, if there is one, must
4072be specified in the UNTIL clause of the `icalendar-rrule' in UTC
4073time.
4074
4075The offset from UTC time when the observance begins is specified
4076in the `icalendar-tzoffsetfrom' property, which is required. The
4077offset from UTC time while the observance is in effect is
4078specified by the `icalendar-tzoffsetto' property, which is also
4079required. A common identifier for the time zone observance can be
4080specified in the `icalendar-tzname' property. Other
4081explanatory comments can be provided in `icalendar-comment'.
4082
4083This component must be a direct child of an `icalendar-vtimezone'
4084component and cannot contain other subcomponents."
4085 :child-spec (:one (ical:dtstart
4086 ical:tzoffsetto
4087 ical:tzoffsetfrom)
4088 :zero-or-one (ical:rrule)
4089 :zero-or-more (ical:comment
4090 ical:rdate
4091 ical:tzname
4092 ical:other-property)
4093 :other-validator ical:rrule-validator)
4094 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
4095
4096(ical:define-component ical:valarm "VALARM"
4097 "Represents an alarm.
4098
4099An alarm is a notification or reminder for an event or task. The
4100type of notification is determined by this component's
4101`icalendar-action' property: it may be an AUDIO, DISPLAY, or
4102EMAIL notification.
4103If it is an audio alarm, it can include an
4104`icalendar-attach' property specifying the audio to be rendered.
4105If it is a DISPLAY alarm, it must include an `icalendar-description'
4106property containing the text to be displayed.
4107If it is an EMAIL alarm, it must include both an
4108`icalendar-summary' and an `icalendar-description', which specify
4109the subject and body of the email, and one or more
4110`icalendar-attendee' properties, which specify the recipients.
4111
4112The required `icalendar-trigger' property specifies when the
4113alarm triggers. If the alarm repeats, then `icalendar-duration'
4114and `icalendar-repeat' properties are also both required.
4115
4116This component must occur as a direct child of an
4117`icalendar-vevent' or `icalendar-vtodo' component, and cannot
4118contain any subcomponents."
4119 :child-spec (:one (ical:action ical:trigger)
4120 :zero-or-one (ical:duration ical:repeat)
4121 :zero-or-more (ical:summary
4122 ical:description
4123 ical:attendee
4124 ical:attach
4125 ical:other-property))
4126 :other-validator ical:valarm-validator
4127 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.6")
4128
4129(defun ical:valarm-validator (node)
4130 "Additional validator function for `icalendar-valarm' components.
4131Checks that NODE has the right properties corresponding to its
4132`icalendar-action' type, e.g., that an EMAIL alarm has a
4133subject (`icalendar-summary') and recipients (`icalendar-attendee').
4134
4135This function is called by `icalendar-ast-node-valid-p' for
4136VALARM nodes; it is not normally necessary to call it directly."
4137 (let* ((action (ical:ast-node-first-child-of 'ical:action node))
4138 (duration (ical:ast-node-first-child-of 'ical:duration node))
4139 (repeat (ical:ast-node-first-child-of 'ical:repeat node))
4140 (child-counts (ical:count-children-by-type node)))
4141
4142 (when (and duration (not repeat))
4143 (ical:signal-validation-error
4144 (concat "`icalendar-valarm' node with `icalendar-duration' "
4145 "must also have `icalendar-repeat' property")
4146 :node node))
4147
4148 (when (and repeat (not duration))
4149 (ical:signal-validation-error
4150 (concat "`icalendar-valarm' node with `icalendar-repeat' "
4151 "must also have `icalendar-duration' property")
4152 :node node))
4153
4154 (let ((action-str (upcase (ical:text-to-string
4155 (ical:ast-node-value action)))))
4156 (cond ((equal "AUDIO" action-str)
4157 (unless (<= (alist-get 'ical:attach child-counts 0) 1)
4158 (ical:signal-validation-error
4159 (concat "AUDIO `icalendar-valarm' may not have "
4160 "more than one `icalendar-attach'")
4161 :node node))
4162 node)
4163
4164 ((equal "DISPLAY" action-str)
4165 (unless (= 1 (alist-get 'ical:description child-counts 0))
4166 (ical:signal-validation-error
4167 (concat "DISPLAY `icalendar-valarm' must have "
4168 "exactly one `icalendar-description'")
4169 :node node))
4170 node)
4171
4172 ((equal "EMAIL" action-str)
4173 (unless (= 1 (alist-get 'ical:summary child-counts 0))
4174 (ical:signal-validation-error
4175 (concat "EMAIL `icalendar-valarm' must have "
4176 "exactly one `icalendar-summary'")
4177 :node node))
4178 (unless (= 1 (alist-get 'ical:description child-counts 0))
4179 (ical:signal-validation-error
4180 (concat "EMAIL `icalendar-valarm' must have "
4181 "exactly one `icalendar-description'")
4182 :node node))
4183 (unless (<= 1 (alist-get 'ical:attendee child-counts 0))
4184 (ical:signal-validation-error
4185 (concat "EMAIL `icalendar-valarm' must have "
4186 "at least one `icalendar-attendee'")
4187 :node node))
4188 node)
4189
4190 (t
4191 ;; "Applications MUST ignore alarms with x-name and iana-token
4192 ;; values they don't recognize." So this is not a validation-error:
4193 (ical:warn
4194 (format "Unknown ACTION value in VALARM: %s" action-str)
4195 :buffer (ical:ast-node-meta-get node :buffer)
4196 :position (ical:ast-node-meta-get node :value-begin))
4197 node)))))
4198
4199(ical:define-component ical:other-component nil
4200 "Component type for unrecognized component names.
4201
4202This component type corresponds to the IANA and X-name components
4203allowed by RFC5545 sec. 3.6; it represents components with an
4204unknown name (matching rx `icalendar-iana-token' or
4205`icalendar-x-name') which must be parsed and preserved but not
4206further interpreted."
4207 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6")
4208
4209;; Technically VCALENDAR is not a "component", but for the
4210;; purposes of parsing and syntax highlighting, it looks just like
4211;; one, so we define it as such here.
4212;; (If this becomes a problem, modify `ical:component-node-p'
4213;; to return nil for VCALENDAR components.)
4214(ical:define-component ical:vcalendar "VCALENDAR"
4215 "Calendar Object.
4216
4217This is the top-level data structure defined by RFC5545. A
4218VCALENDAR must contain the calendar properties `icalendar-prodid'
4219and `icalendar-version', and may contain the calendar properties
4220`icalendar-method' and `icalendar-calscale'.
4221
4222It must also contain at least one VEVENT, VTODO, VJOURNAL,
4223VFREEBUSY, or other component, and for every unique
4224`icalendar-tzidparam' value appearing in a property within these
4225components, the calendar object must contain an
4226`icalendar-vtimezone' defining a time zone with that TZID."
4227 :child-spec (:one (ical:prodid ical:version)
4228 :zero-or-one (ical:calscale ical:method)
4229 :zero-or-more (ical:other-property
4230 ical:vevent
4231 ical:vtodo
4232 ical:vjournal
4233 ical:vfreebusy
4234 ical:vtimezone
4235 ical:other-component))
4236 :other-validator ical:vcalendar-validator
4237 :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.4")
4238
4239(defun ical:all-tzidparams-in (node)
4240 "Recursively find all `icalendar-tzidparam' values in NODE and its children."
4241 (cond ((ical:tzid-param-p node)
4242 (list (ical:ast-node-value node)))
4243 ((ical:param-node-p node)
4244 nil)
4245 (t ;; TODO: could prune search here when properties don't allow tzidparam
4246 (seq-uniq (mapcan #'ical:all-tzidparams-in
4247 (ical:ast-node-children node))))))
4248
4249(defun ical:vcalendar-validator (node)
4250 "Additional validator for `icalendar-vcalendar' NODE.
4251
4252Checks that NODE has at least one component child and that all of the
4253`ical-tzidparam' values appearing in subcomponents have a corresponding
4254`icalendar-vtimezone' definition.
4255
4256This function is called by `icalendar-ast-node-valid-p' for
4257VCALENDAR nodes; it is not normally necessary to call it directly."
4258 (let* ((children (ical:ast-node-children node))
4259 (comp-children (seq-filter #'ical:component-node-p children))
4260 (tz-children (seq-filter #'ical:vtimezone-component-p children))
4261 (defined-tzs
4262 (mapcar
4263 (lambda (tz)
4264 ;; ensure vtimezone component has a TZID property and
4265 ;; extract its string value:
4266 (when (ical:ast-node-valid-p tz)
4267 (ical:with-component tz ((ical:tzid :value-node tzid-text))
4268 (ical:text-to-string tzid-text))))
4269 tz-children))
4270 (appearing-tzids (ical:all-tzidparams-in node)))
4271 (unless comp-children
4272 (ical:signal-validation-error
4273 "`icalendar-vcalendar' must contain at least one component"
4274 :node node))
4275
4276 (let ((seen nil))
4277 (dolist (tzid appearing-tzids)
4278 (unless (member tzid seen)
4279 (unless (member tzid defined-tzs)
4280 (ical:signal-validation-error
4281 (format "No `icalendar-vtimezone' with TZID '%s' in calendar" tzid)
4282 :node node)))
4283 (push tzid seen)))
4284
4285 ;; success:
4286 node))
4287
4288(defun ical:contains-vcalendar-p (&optional buffer)
4289 "Determine whether BUFFER contains \"BEGIN:VCALENDAR\".
4290
4291If so, then BUFFER is a candidate for parsing with, e.g.,
4292`icalendar-parse-calendar'. BUFFER defaults to the current
4293buffer. Returns the position where parsing should start, or nil."
4294 (with-current-buffer (or buffer (current-buffer))
4295 (save-excursion
4296 (goto-char (point-min))
4297 (when (re-search-forward "^BEGIN:VCALENDAR" nil t)
4298 (beginning-of-line)
4299 (point)))))
4300
4301;; `icalendar-parse-component' is sufficient to parse all the syntax in
4302;; a calendar, but a calendar-level parsing function is needed to add
4303;; support for time zones. This function ensures that every
4304;; `icalendar-tzidparam' in the calendar has a corresponding
4305;; `icalendar-vtimezone' component, and modifies the zone information of
4306;; the parsed date-time according to the offset in that time zone.
4307(defun ical:parse-calendar (limit)
4308 "Parse an `icalendar-vcalendar' object from point up to LIMIT.
4309Point should be at the start of the calendar object, i.e., at the start
4310of a line that looks like \"BEGIN:VCALENDAR\". After parsing, point is
4311at the beginning of the next line following the calendar (or end of the
4312buffer). Returns a syntax node representing the calendar."
4313 (require 'icalendar-recur) ; for icr:tz-set-zones-in; avoids circular require
4314 (declare-function icr:tz-set-zones-in "icalendar-recur")
4315 (unless (looking-at-p "^BEGIN:VCALENDAR")
4316 (ical:signal-parse-error "Not at start of VCALENDAR"))
4317 (let ((cal-node (ical:parse-component limit)))
4318 ;(when (ical:ast-node-valid-p cal-node t)
4319 (ical:with-component cal-node
4320 ((ical:vtimezone :all tzs))
4321 ;; After parsing the whole calendar, set the zone and dst slots
4322 ;; in all date-times which are relative to a time zone defined
4323 ;; in the calendar:
4324 ;; (TODO: if this proves too slow in general, we could instead
4325 ;; do it lazily when individual components are queried somehow.
4326 ;; But I'm not convinced that will actually save any time, because
4327 ;; if we're parsing, we're probably already in the middle of a
4328 ;; function that will immediately query all these times, e.g.
4329 ;; `diary-icalendar-import-buffer'.)
4330 (dolist (comp (ical:ast-node-children cal-node))
4331 (unless (ical:vtimezone-component-p comp)
4332 (icr:tz-set-zones-in tzs comp))));)
4333 cal-node))
4334
4335;; TODO: should we do anything to *create* VTIMEZONE nodes in VCALENDAR
4336;; when they're required but don't exist?
4337(defun ical:print-calendar-node (vcalendar)
4338 "Serialize an `icalendar-vcalendar' VCALENDAR to a string.
4339
4340If VCALENDAR is not a valid `icalendar-vcalendar', an
4341`icalendar-validation-error' will be signaled. Any errors that arise
4342during printing will be logged in the buffer returned by
4343`icalendar-error-buffer'."
4344 (when (ical:ast-node-valid-p vcalendar t)
4345 (condition-case err
4346 (ical:print-component-node vcalendar)
4347 (ical:print-error
4348 (ical:handle-print-error err)))))
4349
4350
4351;;; High-level parsing and printing functions.
4352(defun ical:parse (&optional buffer)
4353 "Parse an `icalendar-vcalendar' object in BUFFER (default: current buffer).
4354
4355An unfolded copy of BUFFER (see `icalendar-unfolded-buffer-from-buffer')
4356will first be obtained if necessary. Parsing will begin at the first
4357occurrence of \"BEGIN:VCALENDAR\" in the unfolded buffer.
4358
4359The buffer may be tidied up by user functions before parsing begins; see
4360`icalendar-pre-unfolding-hook' and `icalendar-pre-parsing-hook'.
4361
4362If parsing is successful, the VCALENDAR object is returned. Otherwise,
4363nil is returned, a warning is issued, and errors are logged in the
4364buffer returned by `icalendar-error-buffer'."
4365 (let* ((buf (or buffer (current-buffer)))
4366 (unfolded (cond ((ical:unfolded-p buf) buf)
4367 ((buffer-file-name buf)
4368 (ical:unfolded-buffer-from-file (buffer-file-name buf)))
4369 (t (ical:unfolded-buffer-from-buffer buf)))))
4370 (ical:init-error-buffer)
4371 (with-current-buffer unfolded
4372 (run-hooks 'ical:pre-parsing-hook)
4373 (let ((cal-start (ical:contains-vcalendar-p))
4374 vcalendar)
4375 (unless cal-start
4376 (ical:signal-parse-error "Buffer does not contain \"BEGIN:VCALENDAR\""))
4377 (save-excursion
4378 (goto-char cal-start)
4379 (ical:condition-case err
4380 (setq vcalendar (ical:parse-calendar (point-max)))
4381 (ical:parse-error
4382 (ical:handle-parse-error err)
4383 (warn "Errors while parsing %s; see buffer %s"
4384 buffer (buffer-name (ical:error-buffer))))))
4385 vcalendar))))
4386
4387;; TODO: The function `ical:print' below is not really useful yet.
4388;; Feels like it's needed for completeness but interface needs more thought.
4389;; Should this instead be a generic function that prints any
4390;; kind of node at point? at a given marker?
4391;; What about the coding system? If we want to use this function to print
4392;; iCalendar data to stdout, need to set up coding system correctly and
4393;; perform line folding.
4394;; Etc.
4395;;
4396;; (defun ical:print (vcalendar &optional buffer pos)
4397;; "Insert VCALENDAR as a string at position POS in BUFFER.
4398;;
4399;; VCALENDAR should be an `icalendar-vcalendar'. BUFFER defaults to the
4400;; current buffer and POS defaults to point.
4401;;
4402;; If printing is successful, VCALENDAR is returned. Otherwise, nil is
4403;; returned, a warning is issued, and errors are logged in the buffer
4404;; returned by `icalendar-error-buffer'."
4405;; (with-current-buffer (or buffer (current-buffer))
4406;; (when pos (goto-char pos))
4407;; (condition-case err
4408;; (insert (ical:print-calendar-node vcalendar))
4409;; (ical:print-error
4410;; (ical:handle-print-error err)
4411;; (setq vcalendar nil) ; return
4412;; (warn "Errors while printing; see buffer %s"
4413;; (buffer-name (ical:error-buffer)))))
4414;; vcalendar))
4415
4416
4417;;; Pre-parsing cleanup
4418;;
4419;; The following functions are based on observed syntax errors in
4420;; real-world data and can help clean up such data before parsing.
4421;; More functions can be added here based on user feedback.
4422(defcustom ical:pre-parsing-hook nil
4423 "Hook run by `icalendar-parse' before parsing iCalendar data.
4424
4425If you routinely receive iCalendar data in an incorrect format, you can
4426add functions to this hook which clean up that data before parsing is
4427attempted. The functions in this hook will be run after the iCalendar
4428data has been \"unfolded\" but before parsing begins. (If you need to
4429clean up data before unfolding happens, see
4430`icalendar-pre-unfolding-hook'.)
4431
4432Each function should accept zero arguments and should perform its
4433operation on the entire current buffer."
4434 :version "31.1"
4435 :type '(hook)
4436 :options '(ical:fix-blank-lines
4437 ical:fix-hyphenated-dates
4438 ical:fix-missing-mailtos))
4439
4440(defun ical:fix-blank-lines ()
4441 "Remove blank lines.
4442This function is intended to be used from `icalendar-pre-parsing-hook',
4443which see."
4444 (goto-char (point-min))
4445 (while (re-search-forward (rx "\n" (zero-or-more space) line-end)
4446 nil t)
4447 (replace-match "" nil nil)))
4448
4449(defun ical:fix-hyphenated-dates ()
4450 "Correct dates in \"YYYY-MM-DD...\" format to \"YYYYMMDD...\" format.
4451This function is intended to be used from `icalendar-pre-parsing-hook',
4452which see."
4453 (goto-char (point-min))
4454 (while (re-search-forward
4455 (rx line-start
4456 (or "COMPLETED" "DTEND" "DUE" "DTSTART" "RECURRENCE-ID"
4457 "EXDATE" "RDATE" "CREATED" "DTSTAMP" "LAST-MODIFIED")
4458 (zero-or-more ical:other-param-safe)
4459 ":")
4460 nil t)
4461 (unless (looking-at-p (rx (or ical:date ical:date-time)))
4462 (while (re-search-forward ; exdate, rdate allow lists
4463 (rx (group-n 1 (= 4 digit))
4464 "-"
4465 (group-n 2 (= 2 digit))
4466 "-"
4467 (group-n 3 (= 2 digit)))
4468 (line-end-position) t)
4469 (replace-match "\\1\\2\\3" nil nil)))))
4470
4471(defun ical:fix-missing-mailtos ()
4472 "Insert \"mailto:\" when it is missing before email addresses.
4473This function is intended to be used from `icalendar-pre-parsing-hook',
4474which see."
4475 ;; fix property values in properties that require an address:
4476 (goto-char (point-min))
4477 (while (re-search-forward
4478 (rx line-start (or "ORGANIZER" "ATTENDEE")
4479 (zero-or-more ical:other-param-safe) ":")
4480 nil t)
4481 (unless (looking-at-p (rx ical:cal-address))
4482 (when (looking-at
4483 (rx
4484 ;; match local part of mail address: all the characters
4485 ;; allowed after a URI scheme, *except*
4486 ;; ?@ (so we can match that after) and
4487 ;; ?: (in case we're looking at a non-"mailto:" scheme)
4488 (group-n 1
4489 (one-or-more
4490 (any "A-Za-z0-9" ?- ?. ?_ ?~ ?/ ?? ?# ?\[ ?\] ?! ?$ ?& ?'
4491 ?\( ?\) ?* ?+ ?, ?\; ?= ?%)))
4492 "@"))
4493 (when (or (< (length (match-string 0)) 7)
4494 (not (equal "mailto:"
4495 (substring (downcase (match-string 0)) 0 7))))
4496 (replace-match "mailto:\\1" nil nil nil 1)))))
4497
4498 ;; fix parameter values in parameters that require an address:
4499 (goto-char (point-min))
4500 (while (re-search-forward
4501 (rx line-start ical:name
4502 (zero-or-more icalendar-other-param-safe)
4503 ";"
4504 (or "DELEGATED-FROM" "DELEGATED-TO" "MEMBER" "SENT-BY")
4505 "=")
4506 nil t)
4507 (unless (looking-at-p (rx ical:cal-address))
4508 (while ; DELEGATED* params accept lists
4509 (looking-at
4510 (rx
4511 ?\" ; values of these params must always be quoted
4512 (group-n 1 ; matches local part of mail address as above
4513 (one-or-more
4514 (any "A-Za-z0-9" ?- ?. ?_ ?~ ?/ ?? ?# ?\[ ?\] ?! ?$ ?& ?'
4515 ?\( ?\) ?* ?+ ?, ?= ?%)))
4516 "@"
4517 (zero-or-more (not ?\"))
4518 ?\"
4519 (zero-or-one ",")))
4520 (when (or (< (length (match-string 1)) 7)
4521 (not (equal "mailto:"
4522 (substring (downcase (match-string 1)) 0 7))))
4523 (replace-match "mailto:\\1" nil nil nil 1))
4524 (goto-char (match-end 0))))))
4525
4526
4527;;; Caching and indexing parse trees
4528;;
4529;; The following functions provide a simple in-memory cache and index
4530;; for faster access to parsed iCalendar data by date, UID, and other
4531;; fields of interest. The index and parse tree are stored in a
4532;; buffer-local variable of the parsed buffer and not recomputed if the
4533;; buffer hasn't changed. Most users of the library should just call
4534;; `icalendar-parse-and-index' to get both the parse tree and a
4535;; reference to the index, and get objects of interest from them
4536;; with `icalendar-index-get'.
4537(defun ical:make-index ()
4538 "Create an empty index of iCalendar components."
4539 (list :bydate (make-hash-table :test #'equal) ;; date => list of components
4540 :byuid (make-hash-table :test #'equal) ;; UID => component
4541 :bytzid (make-hash-table :test #'equal) ;; tzid => vtimezone
4542 :recurring (list))) ;; list of components
4543
4544(defun ical:index-insert-tz (index vtimezone)
4545 "Insert VTIMEZONE into INDEX."
4546 (ical:with-component vtimezone
4547 ((ical:tzid :value tzid))
4548 (let ((tzid-index (plist-get index :bytzid)))
4549 (puthash tzid vtimezone tzid-index)
4550 ;; Update and return the index:
4551 (plist-put index :bytzid tzid-index))))
4552
4553
4554(defun ical:index-insert (index component)
4555 "Insert COMPONENT into INDEX."
4556 (require 'icalendar-recur) ; avoid circular imports
4557 (require 'icalendar-utils) ;
4558 (declare-function icr:recurrences-to-count "icalendar-recur")
4559 (declare-function ical:date/time-to-local "icalendar-utils")
4560 (declare-function ical:date/time-to-date "icalendar-utils")
4561 (declare-function ical:dates-until "icalendar-utils")
4562
4563 (ical:with-component component
4564 ((ical:dtstart :first dtstart-node :value dtstart)
4565 (ical:dtend :first dtend-node :value dtend)
4566 (ical:due :value due)
4567 (ical:duration :value duration)
4568 (ical:rrule :value recur-value)
4569 (ical:rdate :all rdate-nodes)
4570 (ical:exdate :all exdate-nodes)
4571 (ical:uid :value uid))
4572 (let ((date-index (plist-get index :bydate))
4573 (uid-index (plist-get index :byuid))
4574 (tzid-index (plist-get index :bytzid))
4575 (recurring (plist-get index :recurring))
4576 (rdates
4577 (mapcar #'ical:ast-node-value
4578 (apply #'append (mapcar #'ical:ast-node-value rdate-nodes))))
4579 (exdates
4580 (mapcar #'ical:ast-node-value
4581 (apply #'append (mapcar #'ical:ast-node-value exdate-nodes))))
4582 dates)
4583 ;; Everything with a UID goes into the uid-index:
4584 (when uid
4585 (puthash uid component uid-index))
4586 ;; For all top-level components, we gather a list of dates on which
4587 ;; they recur for date-index, or put them in the recurring list:
4588 (when dtstart
4589 (cond
4590 ;; If the component has an RRULE that specifies a fixed number
4591 ;; of recurrences, compute them now and index them for each date
4592 ;; in each recurrence:
4593 ((and recur-value (ical:recur-count recur-value))
4594 (let* ((tz (gethash (ical:with-param-of dtstart-node 'ical:tzidparam)
4595 tzid-index))
4596 (recs (cons dtstart (icr:recurrences-to-count component tz))))
4597 (dolist (rec recs)
4598 (let ((end-time
4599 (when duration (ical:date/time-add-duration rec duration))))
4600 (setq dates
4601 (append dates
4602 (if end-time (ical:dates-until rec end-time t)
4603 (list (ical:date/time-to-date
4604 (ical:date/time-to-local rec))))))))))
4605 ;; Same with RDATEs when there's no RRULE:
4606 ((and rdates (not recur-value))
4607 (dolist (rec (cons dtstart rdates))
4608 (unless (or (cl-typep rec 'ical:period) (member rec exdates))
4609 (let ((end-time
4610 (when duration
4611 (ical:date/time-add-duration rec duration))))
4612 (setq dates
4613 (append dates
4614 (if end-time (ical:dates-until rec end-time t)
4615 (list (ical:date/time-to-date
4616 (ical:date/time-to-local rec))))))))
4617 (when (cl-typep rec 'ical:period)
4618 (let* ((start (ical:period-start rec))
4619 (end (or (ical:period-end rec)
4620 (ical:date/time-add-duration
4621 start (ical:period-dur-value rec)))))
4622 (setq dates (append dates (ical:dates-until start end t)))))))
4623 ;; A non-recurring event also gets an index entry for each date
4624 ;; until its end time:
4625 ((not recur-value)
4626 (let ((end-time
4627 (or dtend due
4628 (when duration
4629 (ical:date/time-add-duration dtstart duration)))))
4630 (setq dates (if end-time (ical:dates-until dtstart end-time t)
4631 (list
4632 (ical:date/time-to-date
4633 (ical:date/time-to-local dtstart)))))))
4634 ;; Otherwise, we put off the computation of recurrences until queried:
4635 (t (push component recurring)))
4636
4637 (dolist (date (seq-uniq dates))
4638 (let ((others (gethash date date-index)))
4639 ;; TODO: wonder if we should normalize, and instead store UIDs
4640 ;; in the date index, then look them up by UID when queried.
4641 (puthash date (cons component others) date-index))))
4642
4643 ;; Return the updated index:
4644 (setq index (plist-put index :byuid uid-index))
4645 (setq index (plist-put index :bytzid tzid-index))
4646 (setq index (plist-put index :bydate date-index))
4647 (setq index (plist-put index :recurring recurring))
4648 index)))
4649
4650(defun ical:index-populate-from-calendar (index vcalendar)
4651 "Insert all components in VCALENDAR into INDEX."
4652 (let* ((tzs (ical:ast-node-children-of 'ical:vtimezone vcalendar))
4653 (vevents (ical:ast-node-children-of 'ical:vevent vcalendar))
4654 (vjournals (ical:ast-node-children-of 'ical:vjournal vcalendar))
4655 (vtodos (ical:ast-node-children-of 'ical:vtodo vcalendar))
4656 ;; TODO: customizable selection? what about valarms?
4657 (to-index (append vevents vjournals vtodos)))
4658
4659 ;; First insert the tzs, so that they're available when inserting
4660 ;; the others by date:
4661 (dolist (tz tzs)
4662 (setq index (ical:index-insert-tz index tz)))
4663
4664 (dolist (component to-index)
4665 (setq index (ical:index-insert index component)))
4666 index))
4667
4668(cl-defun ical:index-get (index &rest args &key date uid tzid)
4669 "Get an iCalendar component from INDEX by date, UID, or TZID.
4670
4671INDEX should be a reference to a parse tree index as returned by
4672`icalendar-parse-and-index', which see. The index can be queried by:
4673
4674:uid UID (string, see `icalendar-uid') - returns the component with that
4675 UID.
4676
4677:tzid TZID (string, see `icalendar-tzid' and `icalendar-tzidparam') -
4678 returns the `icalendar-vtimezone' component with that TZID.
4679
4680:date DT (an `icalendar-date', i.e. a list (M D Y)) - returns a list of
4681 the components occurring (or recurring) on that date.
4682
4683Only one keyword argument can be queried at a time."
4684 (require 'icalendar-recur) ; avoid circular imports
4685 (require 'icalendar-utils) ;
4686
4687 (declare-function icr:find-interval "icalendar-recur")
4688 (declare-function icr:recurrences-in-interval "icalendar-recur")
4689 (declare-function ical:date/time-in-period-p "icalendar-utils")
4690 (declare-function ical:date/time<= "icalendar-utils")
4691 (declare-function ical:date/time< "icalendar-utils")
4692 (declare-function ical:date/time-add-duration "icalendar-utils")
4693
4694 (when (length> args 2)
4695 (error "Only one keyword argument can be queried"))
4696 (cond (uid (gethash uid (plist-get index :byuid)))
4697 (tzid (gethash tzid (plist-get index :bytzid)))
4698 (date
4699 (let ((computed (gethash date (plist-get index :bydate)))
4700 (recurring (plist-get index :recurring)))
4701 (dolist (component recurring)
4702 (ical:with-component component
4703 ((ical:dtstart :first dtstart-node :value dtstart)
4704 (ical:rrule :value recur-value)
4705 (ical:rdate :all rdate-nodes)
4706 (ical:duration :value duration))
4707 (unless (ical:date/time<= date dtstart)
4708 (let* ((tz (ical:with-param-of dtstart-node 'ical:tzidparam nil
4709 (gethash value (plist-get index :bytzid))))
4710 (int (icr:find-interval date dtstart recur-value tz))
4711 (recs (icr:recurrences-in-interval int component tz)))
4712 (catch 'found
4713 (dolist (rec recs)
4714 (let* ((local-rec (ical:date/time-to-local rec))
4715 (end
4716 (when duration
4717 (ical:date/time-add-duration local-rec duration)))
4718 (rec-dates
4719 (if end (ical:dates-until local-rec end t)
4720 (list (ical:date/time-to-date local-rec)))))
4721 (when (member date rec-dates)
4722 (push component computed)
4723 (throw 'found nil))))
4724 (dolist (node rdate-nodes)
4725 ;; normal RDATE recurrences have already been
4726 ;; checked above, but we check whether `date'
4727 ;; occurs in any RDATE period values here:
4728 (when (eq 'ical:period
4729 (ical:value-type-from-params
4730 (ical:ast-node-children node)))
4731 (let* ((tz
4732 (ical:with-param-of node 'ical:tzidparam nil
4733 (gethash value (plist-get index :bytzid)))))
4734 (ical:with-property node nil
4735 (dolist (period values)
4736 (when (ical:date/time-in-period-p date period tz)
4737 (push component computed)
4738 (throw 'found nil))))))))))))
4739 computed))
4740 (t (error "At least one of :uid, :tzid, or :date is required"))))
4741
4742;; Buffer local variable to cache the index and parse tree.
4743;; Format: (TICKS VCALENDAR INDEX)
4744;; TICKS is the value of (buffer-modified-tick) at last parse
4745(defvar-local ical:-parsed-calendar-and-index '(0 nil nil))
4746
4747(defun ical:parse-and-index (&optional buffer-or-file)
4748 "Parse and index the first iCalendar VCALENDAR object in BUFFER-OR-FILE.
4749
4750Returns a list (VCALENDAR INDEX), where VCALENDAR is the parsed
4751`icalendar-vcalendar' syntax tree. The index can then be queried to
4752retrieve components from this calendar by UID, TZID, or date; see
4753`icalendar-index-get'.
4754
4755BUFFER-OR-FILE may be a buffer or a string containing a filename; it
4756defaults to the current buffer. If it is a filename, an unfolded buffer
4757containing its data will be found, or created if necessary (see
4758`icalendar-unfolded-buffer-from-file'). The resulting buffer must
4759contain an iCalendar VCALENDAR object, which will be parsed and indexed.
4760
4761The results of parsing and indexing are cached in buffer-local
4762variables, and subsequent calls with the same BUFFER-OR-FILE will return
4763the cached results as long as the buffer has not been modified in the
4764meantime."
4765 (let* ((buffer (cond ((null buffer-or-file) (current-buffer))
4766 ((bufferp buffer-or-file) buffer-or-file)
4767 ((and (stringp buffer-or-file)
4768 (file-exists-p buffer-or-file))
4769 (find-buffer-visiting buffer-or-file))))
4770 (file-name (cond (buffer (buffer-file-name buffer))
4771 ((and (stringp buffer-or-file)
4772 (file-exists-p buffer-or-file))
4773 (expand-file-name buffer-or-file))))
4774 (unfolded (cond ((and buffer (ical:unfolded-p buffer))
4775 buffer)
4776 (file-name
4777 (or (ical:find-unfolded-buffer-visiting file-name)
4778 (ical:unfolded-buffer-from-file file-name)))
4779 (buffer
4780 (ical:unfolded-buffer-from-buffer buffer))
4781 (t
4782 (error "Unable to get unfolded buffer for '%s'"
4783 buffer-or-file)))))
4784 (with-current-buffer unfolded
4785 (when (ical:contains-vcalendar-p)
4786 (if (eql (car ical:-parsed-calendar-and-index) (buffer-modified-tick))
4787 (cdr ical:-parsed-calendar-and-index)
4788 (message "Parsing and indexing iCalendar data in %s..." (buffer-name))
4789 (let ((vcalendar (ical:parse)))
4790 (when vcalendar
4791 (setq ical:-parsed-calendar-and-index
4792 (list
4793 (buffer-modified-tick)
4794 vcalendar
4795 (ical:index-populate-from-calendar (ical:make-index)
4796 vcalendar)))
4797 (message "Parsing and indexing iCalendar data in %s...Done."
4798 (buffer-name))
4799 (cdr ical:-parsed-calendar-and-index))))))))
4800
4801
4802
4803;;; Documentation for all of the above via `describe-symbol':
4804(defun ical:documented-symbol-p (sym)
4805 "Return non-nil if SYM is a symbol with iCalendar documentation."
4806 (or (get sym 'icalendar-type-documentation)
4807 ;; grammatical categories defined with rx-define, but with no
4808 ;; other special icalendar docs:
4809 (and (get sym 'rx-definition)
4810 (length> (symbol-name sym) 10)
4811 (equal "icalendar-" (substring (symbol-name sym) 0 10)))))
4812
4813(defun ical:documentation (sym buf frame)
4814 "iCalendar documentation backend for `describe-symbol-backends'."
4815 (ignore buf frame) ; Silence the byte compiler
4816 (with-help-window (help-buffer)
4817 (with-current-buffer standard-output
4818 (let* ((type-doc (get sym 'icalendar-type-documentation))
4819 (link (get sym 'icalendar-link))
4820 (rx-def (get sym 'rx-definition))
4821 (rx-doc (when rx-def
4822 (with-output-to-string
4823 (pp rx-def))))
4824 (value-rx-def (get sym 'ical:value-rx))
4825 (value-rx-doc (when value-rx-def
4826 (with-output-to-string
4827 (pp value-rx-def))))
4828 (values-rx-def (get sym 'ical:values-rx))
4829 (values-rx-doc (when values-rx-def
4830 (with-output-to-string
4831 (pp values-rx-def))))
4832
4833 (full-doc
4834 (concat
4835 (when type-doc
4836 (format "`%s' is an iCalendar type:\n\n%s\n\n"
4837 sym type-doc))
4838 (when link
4839 (format "For further information see\nURL `%s'\n\n" link))
4840 ;; FIXME: this is probably better done in rx.el!
4841 ;; TODO: could also generalize this to recursively
4842 ;; search rx-def for any symbol that starts with "icalendar-"...
4843 (when rx-def
4844 (format "`%s' is an iCalendar grammar category.
4845Its `rx' definition is:\n\n%s%s%s"
4846 sym
4847 rx-doc
4848 (if value-rx-def
4849 (format "\nIndividual values must match:\n%s"
4850 value-rx-doc)
4851 "")
4852 (if values-rx-def
4853 (format "\nLists of values must match:\n%s"
4854 values-rx-doc)
4855 "")))
4856 "\n")))
4857
4858 (insert full-doc)
4859 full-doc))))
4860
4861
4862(defconst ical:describe-symbol-backend
4863 '(nil icalendar-documented-symbol-p icalendar-documentation)
4864 "Entry for icalendar documentation in `describe-symbol-backends'.")
4865
4866(push ical:describe-symbol-backend describe-symbol-backends)
4867
4868;; Unloading:
4869(defun ical:parser-unload-function ()
4870 "Unload function for `icalendar-parser'."
4871 (mapatoms
4872 (lambda (sym)
4873 (when (string-match "^icalendar-" (symbol-name sym))
4874 (makunbound sym)
4875 (fmakunbound sym))))
4876
4877 (setq describe-symbol-backends
4878 (remq ical:describe-symbol-backend describe-symbol-backends))
4879 ;; Proceed with normal unloading:
4880 nil)
4881
4882(provide 'icalendar-parser)
4883
4884;; Local Variables:
4885;; read-symbol-shorthands: (("ical:" . "icalendar-") ("icr:" . "icalendar-recur-"))
4886;; End:
4887;;; icalendar-parser.el ends here
diff --git a/lisp/calendar/icalendar-recur.el b/lisp/calendar/icalendar-recur.el
new file mode 100644
index 00000000000..28a05aacf7c
--- /dev/null
+++ b/lisp/calendar/icalendar-recur.el
@@ -0,0 +1,2148 @@
1;;; icalendar-recur.el --- Support for iCalendar recurrences and time zones -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Richard Lawrence
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: December 2024
7;; Keywords: calendar
8
9;; This file is part of GNU Emacs.
10
11;; This file is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15
16;; This file is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with this file. If not, see <https://www.gnu.org/licenses/>.
23
24;;; Commentary:
25
26;; This is a sub-library for working with recurrence rules and time
27;; zones, as defined by RFC5545 (see especially Secs. 3.3.10 and
28;; 3.8.5.3, which are required reading before you make any changes to
29;; the code below) and related standards (especially RFC8984 Sec. 4.3,
30;; also strongly recommended reading). Recurrence rules and time zones
31;; are mutually dependent: to calculate the date and time of future
32;; instances of a recurring event, you must be able to apply time zone
33;; rules; and to apply time zone rules, you must be able to calculate
34;; the date and time of recurring events, namely the shifts between
35;; observances of standard and daylight savings time. For example, an
36;; event that occurs "on the last Friday of every month at 11AM" in a
37;; given time zone should recur at 11AM daylight savings time in July,
38;; but 11AM standard time in January, for a typical time zone that
39;; shifts from standard to DST and back once each year. These shifts
40;; occur at, say, "the last Sunday in March at 2AM" and "the first
41;; Sunday in November at 2AM". So to calculate an absolute time for a
42;; given instance of the original event, you first have to calculate the
43;; nearest instance of the shift between standard and daylight savings
44;; time, which itself involves applying a recurrence rule of the same
45;; form.
46;;
47;; This mutual dependence between recurrence rules and time zones is not
48;; a *vicious* circle, because the shifts between time zone observances
49;; have fixed offsets from UTC time which are made explicit in iCalendar
50;; data. But it does make things complicated. RFC5545 focuses on making
51;; recurrence rules expressive enough to cover existing practices,
52;; including time zone observance shifts, rather than on being easy to
53;; implement.
54;;
55;; So be forewarned: here be dragons. The code here was difficult to get
56;; working, in part because this mutual dependence means it is difficult
57;; to implement anything less than the whole system, in part because
58;; recurrence rules are very flexible in order to cover as many
59;; practical uses as possible, in part because time zone practices are
60;; themselves complicated, and in part because there are a *lot* of edge
61;; cases to worry about. Much of it is tedious and repetitive but
62;; doesn't lend itself to further simplification or abstraction. If you
63;; need to make changes, make them slowly, and use the tests in
64;; test/lisp/calendar/icalendar-recur-tests.el to make sure they don't
65;; break anything.
66;;
67;; Notation: `date/time' with a slash in symbol names means "`date' or
68;; `date-time'", i.e., is a way of indicating that a function can
69;; accept either type of value, and `dt' is typically used for an
70;; argument of either type. `date-time' should always refer to *just*
71;; date-time values, not plain (calendar-style) dates.
72
73;;; Code:
74(require 'icalendar-ast)
75(require 'icalendar-parser)
76(require 'icalendar-utils)
77(require 'cl-lib)
78(require 'calendar)
79(require 'cal-dst)
80(require 'simple)
81(require 'seq)
82(eval-when-compile '(require 'icalendar-macs))
83
84
85;; Recurrence Intervals
86;;
87;; Two important ideas in the following:
88;;
89;; 1) Because recurrence sets are potentially infinite, we always
90;; calculate recurrences within certain upper and lower bounds. These
91;; bounds might be determined by a user interface (e.g. the week or
92;; month displayed in a calendar) or might be derived from the logic of
93;; the recurrence rule itself. In the former case, where the bounds can
94;; be arbitrary, it's called a 'window' here (as in "window of
95;; time"). In the latter case, it's called an 'interval' here (after the
96;; "INTERVAL=..." clause in recurrence rules).
97;;
98;; Unlike a window, an interval must be synced up with the recurrence
99;; rule: its bounds must fall at successive integer multiples of the
100;; product of the recurrence rule's FREQ and INTERVAL values, relative
101;; to a starting date/time. For example, a recurrence rule with a
102;; MONTHLY frequency and INTERVAL=3 will have an interval that is three
103;; months long. If its start date is, e.g., in November, then the first
104;; interval runs from November to February, the next from February to
105;; May, and so on. Because intervals depend only on the starting
106;; date/time, the frequency, and the interval length, it is relatively
107;; straightforward to compute the bounds of the interval surrounding an
108;; arbitrary point in time (without enumerating them successively from
109;; the start time); see `icalendar-recur-find-interval', which calls
110;; this arbitrary point in time the 'target'.
111;;
112;; 2) An interval is the smallest unit of time for which we compute
113;; values of the recurrence set. This is because the "BYSETPOS=..."
114;; clause in a recurrence rule operates on the sequence of recurrences
115;; in a single interval. Since it selects recurrences by their index in
116;; this sequence, the sequence must have a determinate length and known
117;; bounds. The function `icalendar-recur-recurrences-in-interval' is the
118;; main function to compute recurrences in a given interval.
119;;
120;; The way to compute the recurrences in an arbitrary *window* is thus
121;; to find the interval bounds which are closest to the window's lower
122;; and upper bound, and then compute the recurrences for all the
123;; intervals in between, i.e., that "cover" the window. This is what the
124;; function `icalendar-recur-recurrences-in-window' does.
125;;
126;; Note that the recurrence set for a recurrence rule with a COUNT
127;; clause cannot be computed for an arbitrary interval (or window);
128;; instead, the set must be enumerated from the beginning, so that the
129;; enumeration can stop after a fixed number of recurrences. This is
130;; what the function `icalendar-recur-recurrences-to-count' does. But
131;; also in this case, recurrences are generated for one interval at a
132;; time, because a BYSETPOS clause might apply.
133;;
134;; An interval is represented as a list (LOW HIGH NEXT-LOW) of decoded
135;; times. The length of time between LOW and HIGH corresponds to the
136;; FREQ rule part: they are one year apart for a 'YEARLY rule, a month
137;; apart for a 'MONTHLY rule, etc. NEXT-LOW is the upper bound of the
138;; interval: it is equal to LOW in the subsequent interval. When the
139;; INTERVAL rule part is equal to 1 (the default), HIGH and NEXT-LOW are
140;; the same, but if it is > 1, NEXT-LOW is equal to LOW + INTERVAL *
141;; FREQ. For example, in a 'MONTHLY rule where INTERVAL=3, which means
142;; "every three months", LOW and HIGH bound the first month, while HIGH
143;; and NEXT-LOW bound the following two months.
144;;
145;; The times between LOW and HIGH are candidates for recurrences. LOW
146;; is an inclusive lower bound, and HIGH is an exclusive upper bound:
147;; LOW <= R < HIGH for each recurrence R in the interval. The times
148;; between HIGH and NEXT-LOW are not candidates for recurrences.
149;;
150;; The following functions deal with constructing intervals, given a
151;; target, a start date/time, and intervalsize, and optionally a time
152;; zone. The main entry point is `icalendar-recur-find-interval'.
153
154;; Look, dragons already:
155(defun icr:find-absolute-interval (target dtstart intervalsize freqs
156 &optional vtimezone)
157 "Find a recurrence interval based on a fixed number of seconds.
158
159INTERVALSIZE should be the total size of the interval in seconds. FREQS
160should be the number of seconds between the lower bound of the interval
161and the upper bound for candidate recurrences; it is the number of
162seconds in the unit of time in a recurrence rule's FREQ part. The
163returned interval looks like (LOW LOW+FREQS LOW+INTERVALSIZE). See
164`icalendar-recur-find-interval' for other arguments' meanings."
165 ;; We assume here that the interval needs to be calculated using
166 ;; absolute times for SECONDLY, MINUTELY, and HOURLY rules.
167 ;; There are two reasons for this:
168 ;;
169 ;; 1) Time zone shifts. If we don't use absolute times, and instead
170 ;; find interval boundaries using local clock times with e.g.
171 ;; `ical:date/time-add' (as we do with time units of a day or
172 ;; greater below), we have to adjust for clock time changes. Using
173 ;; absolute times is simpler.
174 ;; 2) More problematically, using local clock times, at least in its
175 ;; most straightforward implementation, has pathological results
176 ;; when `intervalsize' is relatively prime with 60 (for a SECONDLY
177 ;; rule, similarly for the others): intervals generated by
178 ;; successive enumeration from one target value will not in general
179 ;; align with intervals generated from a different, but nearby,
180 ;; target value. (So going this route seems to mean giving up on
181 ;; the idea that intervals can be calculated just from `target',
182 ;; `dtstart' and `intervalsize', and instead always enumerating
183 ;; them from the beginning.)
184 ;;
185 ;; In effect, we are deciding that a rule like "every 3 hours" always
186 ;; means every 3 * 60 * 60 = 10800 seconds after `dtstart', and not
187 ;; "every 10800 seconds, except when there's a time zone observance
188 ;; change". People who want the latter have another option: use a
189 ;; DAILY rule and specify the (local) times for the hours they want in
190 ;; the BYHOUR clause, etc. (People who want it for a number of hours,
191 ;; e.g. 7, which does not divide 24, unfortunately do *not* have this
192 ;; option, but anyone who wants that but does not want to understand
193 ;; "7 hours" as a fixed number of seconds has a pathology that I
194 ;; cannot cure here.)
195 ;;
196 ;; RFC5545 does not seem to pronounce one way or the other on whether
197 ;; this decision is correct: there are no examples of SECONDLY rules
198 ;; to go on, and the few examples for MINUTELY and HOURLY rules only
199 ;; use "nice" values in the INTERVAL clause (real-life examples
200 ;; probably(?) will too). Our assumption has some possibly
201 ;; unintuitive consequences for `intervalsize' values that are not
202 ;; "nice" (basically, whenever intervalsize and either 60 or 24 are
203 ;; relatively prime), and for how interval boundaries behave at the
204 ;; shifts between time zone observances (since local clock times in
205 ;; the interval bounds will shift from what they would have been
206 ;; before the observance change -- arguably correct but possibly
207 ;; surprising, depending on the case). But the alternative seems
208 ;; worse, so until countervailing evidence emerges, this approach
209 ;; seems reasonable.
210 (let* ((given-start-zone (decoded-time-zone dtstart))
211 (start-w/zone (cond (given-start-zone dtstart)
212 ((ical:vtimezone-component-p vtimezone)
213 (ical:date-time-variant dtstart :tz vtimezone))
214 (t
215 ;; "Floating" time should be interpreted in user's
216 ;; current time zone; see RFC5545 Sec 3.3.5
217 (ical:date-time-variant
218 dtstart :zone (car (current-time-zone))))))
219 (start-abs (ignore-errors
220 (time-convert (encode-time start-w/zone) 'integer)))
221 (given-target-zone (decoded-time-zone target))
222 (target-w/zone (cond (given-target-zone target)
223 (vtimezone
224 (ical:date-time-variant target :tz vtimezone))
225 (t
226 (ical:date-time-variant
227 target :zone (car (current-time-zone))))))
228 (target-abs (ignore-errors
229 (time-convert (encode-time target-w/zone) 'integer)))
230 low-abs low high next-low)
231
232 (unless (zerop (mod intervalsize freqs))
233 ;; Bad things will happen if intervalsize is not an integer
234 ;; multiple of freqs
235 (error "FREQS=%d does not divide INTERVALSIZE=%d" freqs intervalsize))
236 (unless (and start-abs target-abs)
237 (when (not start-abs)
238 (error "Could not determine an offset for DTSTART=%s" dtstart))
239 (when (not target-abs)
240 (error "Could not determine an offset for TARGET=%s" target)))
241
242 ;; Find the lower bound below target that is the closest integer
243 ;; multiple of intervalsize seconds from dtstart
244 (setq low-abs (- target-abs
245 (mod (- target-abs start-abs) intervalsize)))
246
247 (if vtimezone
248 (setq low (icr:tz-decode-time low-abs vtimezone)
249 high (icr:tz-decode-time (+ low-abs freqs) vtimezone)
250 next-low (icr:tz-decode-time (+ low-abs intervalsize) vtimezone))
251 ;; best we can do is decode into target's zone:
252 (let ((offset (decoded-time-zone target-w/zone)))
253 (setq low (icr:tz-decode-time low-abs offset)
254 high (icr:tz-decode-time (+ low-abs freqs) offset)
255 next-low (icr:tz-decode-time (+ low-abs intervalsize) offset))))
256
257 (unless (and given-start-zone given-target-zone)
258 ;; but if we started with floating times, we should return floating times:
259 (setf (decoded-time-zone low) nil)
260 (setf (decoded-time-dst low) -1)
261 (setf (decoded-time-zone high) nil)
262 (setf (decoded-time-dst high) -1)
263 (setf (decoded-time-zone next-low) nil)
264 (setf (decoded-time-dst next-low) -1))
265
266 (list low high next-low)))
267
268(defun icr:find-secondly-interval (target dtstart intervalsize &optional vtimezone)
269 "Find a SECONDLY recurrence interval.
270See `icalendar-recur-find-interval' for arguments' meanings."
271 (icr:find-absolute-interval
272 target
273 dtstart
274 intervalsize
275 1
276 vtimezone))
277
278(defun icr:find-minutely-interval (target dtstart intervalsize &optional vtimezone)
279 "Find a MINUTELY recurrence interval.
280See `icalendar-recur-find-interval' for arguments' meanings."
281 (icr:find-absolute-interval
282 target
283 ;; A MINUTELY interval always runs from the beginning of a minute to
284 ;; the beginning of the next minute:
285 (ical:date-time-variant dtstart :second 0 :tz 'preserve)
286 (* 60 intervalsize)
287 60
288 vtimezone))
289
290(defun icr:find-hourly-interval (target dtstart intervalsize &optional vtimezone)
291 "Find an HOURLY recurrence interval.
292See `icalendar-recur-find-interval' for arguments' meanings."
293 (icr:find-absolute-interval
294 target
295 ;; An HOURLY interval always runs from the beginning of an hour to
296 ;; the beginning of the next hour:
297 (ical:date-time-variant dtstart :minute 0 :second 0 :tz 'preserve)
298 (* 60 60 intervalsize)
299 (* 60 60)
300 vtimezone))
301
302(defun icr:find-daily-interval (target dtstart intervalsize &optional vtimezone)
303 "Find a DAILY recurrence interval.
304See `icalendar-recur-find-interval' for arguments' meanings."
305 (let* ((start-absdate (calendar-absolute-from-gregorian
306 (ical:date/time-to-date dtstart)))
307 (target-absdate (calendar-absolute-from-gregorian
308 (ical:date/time-to-date target)))
309 ;; low-absdate is the closest absolute date below target that
310 ;; is an integer multiple of intervalsize days from dtstart
311 (low-absdate (- target-absdate
312 (mod (- target-absdate start-absdate) intervalsize)))
313 (high-absdate (1+ low-absdate))
314 (next-low-absdate (+ low-absdate intervalsize)))
315
316 (let* ((low-dt (ical:date-to-date-time
317 (calendar-gregorian-from-absolute low-absdate)))
318 (high-dt (ical:date-to-date-time
319 (calendar-gregorian-from-absolute high-absdate)))
320 (next-low-dt (ical:date-to-date-time
321 (calendar-gregorian-from-absolute next-low-absdate))))
322
323 (when vtimezone
324 (icr:tz-set-zone low-dt vtimezone)
325 (icr:tz-set-zone high-dt vtimezone)
326 (icr:tz-set-zone next-low-dt vtimezone))
327
328 ;; Return the bounds:
329 (list low-dt high-dt next-low-dt))))
330
331(defun icr:find-weekly-interval (target dtstart intervalsize
332 &optional weekstart vtimezone)
333 "Find a WEEKLY recurrence interval.
334See `icalendar-recur-find-interval' for arguments' meanings."
335 (let* ((target-date (ical:date/time-to-date target))
336 (start-date (ical:date/time-to-date dtstart))
337 ;; the absolute dates of the week start before target and
338 ;; dtstart; these are always a whole number of weeks apart:
339 (target-week-abs (calendar-nth-named-absday
340 -1
341 (or weekstart 1)
342 (calendar-extract-month target-date)
343 (calendar-extract-year target-date)
344 (calendar-extract-day target-date)))
345 (start-abs (calendar-nth-named-absday
346 -1
347 (or weekstart 1)
348 (calendar-extract-month start-date)
349 (calendar-extract-year start-date)
350 (calendar-extract-day start-date)))
351 (intsize-days (* 7 intervalsize))
352 ;; the absolute date of the week start before target which is
353 ;; an integer multiple of intervalsize weeks from dtstart:
354 (low-abs (- target-week-abs
355 (mod (- target-week-abs start-abs) intsize-days)))
356 ;; then use this to find the interval bounds:
357 (low (ical:date-to-date-time
358 (calendar-gregorian-from-absolute low-abs)))
359 (high (ical:date-to-date-time
360 (calendar-gregorian-from-absolute (+ 7 low-abs))))
361 (next-low (ical:date-to-date-time
362 (calendar-gregorian-from-absolute (+ intsize-days low-abs)))))
363
364 (when vtimezone
365 (icr:tz-set-zone low vtimezone)
366 (icr:tz-set-zone high vtimezone)
367 (icr:tz-set-zone next-low vtimezone))
368
369 ;; Return the bounds:
370 (list low high next-low)))
371
372(defun icr:find-monthly-interval (target dtstart intervalsize &optional vtimezone)
373 "Find a MONTHLY recurrence interval.
374See `icalendar-recur-find-interval' for arguments' meanings."
375 (let* ((start-month (ical:date/time-month dtstart))
376 (start-year (ical:date/time-year dtstart))
377 ;; we calculate in "absolute months", i.e., number of months
378 ;; since the beginning of the Gregorian calendar, to make
379 ;; finding the lower bound easier:
380 (start-abs-months (+ (* 12 (1- start-year)) (1- start-month)))
381 (target-month (ical:date/time-month target))
382 (target-year (ical:date/time-year target))
383 (target-abs-months (+ (* 12 (1- target-year)) (1- target-month)))
384 ;; number of "absolute months" between start of dtstart's month
385 ;; and start of target's month:
386 (nmonths (- target-abs-months start-abs-months))
387 ;; the number of months after dtstart that is the closest integer
388 ;; multiple of intervalsize months before target:
389 (lmonths (- nmonths (mod nmonths intervalsize)))
390 ;; convert these "absolute months" back to Gregorian month and year:
391 (mod-month (mod (+ start-month lmonths) 12))
392 (low-month (if (zerop mod-month) 12 mod-month))
393 (low-year (+ (/ lmonths 12) start-year
394 ;; iff we cross a year boundary moving forward in
395 ;; time from start-month to target-month, we need
396 ;; to add one to the year:
397 (if (<= start-month target-month) 0 1)))
398 ;; and now we can use these to calculate the interval bounds:
399 (low (ical:make-date-time :year low-year :month low-month :day 1
400 :hour 0 :minute 0 :second 0 :tz vtimezone))
401 (high (ical:date/time-add low :month 1 vtimezone))
402 (next-low (ical:date/time-add low :month intervalsize vtimezone)))
403
404 ;; Return the bounds:
405 (list low high next-low)))
406
407(defun icr:find-yearly-interval (target dtstart intervalsize &optional vtimezone)
408 "Find a YEARLY recurrence interval.
409See `icalendar-recur-find-interval' for arguments' meanings."
410 (let* ((start-year (ical:date/time-year dtstart))
411 (target-year (ical:date/time-year target))
412 ;; The year before target that is the closest integer multiple
413 ;; of intervalsize years after dtstart:
414 (low-year (- target-year
415 (mod (- target-year start-year) intervalsize)))
416 (low (ical:make-date-time :year low-year :month 1 :day 1
417 :hour 0 :minute 0 :second 0 :tz vtimezone))
418 (high (ical:make-date-time :year (1+ low-year) :month 1 :day 1
419 :hour 0 :minute 0 :second 0 :tz vtimezone))
420 (next-low (ical:make-date-time :year (+ low-year intervalsize)
421 :month 1 :day 1 :hour 0 :minute 0 :second 0
422 :tz vtimezone)))
423
424 ;; Return the bounds:
425 (list low high next-low)))
426
427(defun icr:find-interval (target dtstart recur-value &optional vtimezone)
428 "Return the recurrence interval around TARGET.
429
430TARGET and DTSTART should be `icalendar-date' or `icalendar-date-time'
431values. RECUR-VALUE should be an `icalendar-recur'.
432
433The returned value is a list (LOW HIGH NEXT-LOW) which
434represents the lower and upper bounds of a recurrence interval around
435TARGET. For some N, LOW is equal to START + N*INTERVALSIZE units, HIGH
436is equal to START + (N+1)*INTERVALSIZE units, and LOW <= TARGET < HIGH.
437START here is a time derived from DTSTART depending on RECUR-VALUE's
438FREQ part: the first day of the year for a \\='YEARLY rule, first day
439of the month for a \\='MONTHLY rule, etc.
440
441RECUR-VALUE's interval determines INTERVALSIZE, and its frequency
442determines the units: a month for \\='MONTHLY, etc.
443
444If VTIMEZONE is provided, it is used to set time zone information in the
445returned interval bounds. Otherwise, the bounds contain no time zone
446information and represent floating local times."
447 (let ((freq (ical:recur-freq recur-value))
448 (intsize (ical:recur-interval-size recur-value))
449 (weekstart (ical:recur-weekstart recur-value)))
450 (cl-case freq
451 (SECONDLY (icr:find-secondly-interval target dtstart intsize vtimezone))
452 (MINUTELY (icr:find-minutely-interval target dtstart intsize vtimezone))
453 (HOURLY (icr:find-hourly-interval target dtstart intsize vtimezone))
454 (DAILY (icr:find-daily-interval target dtstart intsize vtimezone))
455 (WEEKLY (icr:find-weekly-interval target dtstart intsize
456 weekstart vtimezone))
457 (MONTHLY (icr:find-monthly-interval target dtstart intsize vtimezone))
458 (YEARLY (icr:find-yearly-interval target dtstart intsize vtimezone)))))
459
460(defun icr:nth-interval (n dtstart recur-value &optional vtimezone)
461 "Return the Nth recurrence interval after DTSTART.
462
463The returned value is a list (LOW HIGH NEXT-LOW) which represent the Nth
464recurrence interval after DTSTART. LOW is equal to START +
465N*INTERVALSIZE units, HIGH is equal to START + (N+1)*INTERVALSIZE units,
466and LOW <= TARGET < HIGH. START here is a time derived from DTSTART
467depending on RECUR-VALUE's FREQ part: the first day of the year for a
468\\='YEARLY rule, first day of the month for a \\='MONTHLY rule, etc.
469
470RECUR-VALUE's interval determines INTERVALSIZE, and its frequency
471determines the units: a month for \\='MONTHLY, etc.
472
473N should be a non-negative integer. Interval 0 is the interval
474containing DTSTART. DTSTART should be an `icalendar-date' or
475`icalendar-date-time' value. RECUR-VALUE should be an
476`icalendar-recur'.
477
478If VTIMEZONE is provided, it is used to set time zone information in the
479returned interval bounds. Otherwise, the bounds contain no time zone
480information and represent floating local times."
481 (when (< n 0) (error "Recurrence interval undefined for negative N"))
482 (let* ((start-dt (if (cl-typep dtstart 'ical:date)
483 (ical:date-to-date-time dtstart :tz vtimezone)
484 dtstart))
485 (freq (ical:recur-freq recur-value))
486 (intervalsize (ical:recur-interval-size recur-value))
487 (unit (cl-case freq
488 (YEARLY :year)
489 (MONTHLY :month)
490 (WEEKLY :week)
491 (DAILY :day)
492 (HOURLY :hour)
493 (MINUTELY :minute)
494 (SECONDLY :second)))
495 (target (ical:date/time-add start-dt unit (* n intervalsize) vtimezone)))
496 (icr:find-interval target dtstart recur-value vtimezone)))
497
498(defun icr:next-interval (interval recur-value &optional vtimezone)
499 "Return the next recurrence interval after INTERVAL.
500
501Given a recurrence interval (LOW HIGH NEXT), returns the next interval
502\(NEXT HIGHER HIGHER-NEXT), where HIGHER and HIGHER-NEXT are determined
503by the frequency and interval sizes of RECUR-VALUE."
504 (let* ((new-low (caddr interval))
505 (freq (ical:recur-freq recur-value))
506 (unit (cl-case freq
507 (YEARLY :year)
508 (MONTHLY :month)
509 (WEEKLY :week)
510 (DAILY :day)
511 (HOURLY :hour)
512 (MINUTELY :minute)
513 (SECONDLY :second)))
514 (intervalsize (ical:recur-interval-size recur-value))
515 (new-high (ical:date/time-add new-low unit 1 vtimezone))
516 (new-next (ical:date/time-add new-low unit intervalsize vtimezone)))
517
518 (when vtimezone
519 (icr:tz-set-zone new-low vtimezone)
520 ;; (icr:tz-set-zone new-high vtimezone)
521 ;; (icr:tz-set-zone new-next vtimezone)
522 )
523
524 (list new-low new-high new-next)))
525
526(defun icr:previous-interval (interval recur-value dtstart &optional vtimezone)
527 "Given a recurrence INTERVAL, return the previous interval.
528
529For an interval (LOW HIGH NEXT-LOW), the previous interval is
530\(PREV-LOW PREV-HIGH LOW), where PREV-LOW and PREV-HIGH are determined by
531the frequency and interval sizes of RECUR-VALUE (see
532`icalendar-recur-find-interval'). If the resulting period of time
533between PREV-LOW and PREV-HIGH occurs entirely before DTSTART, then the
534interval does not exist; in this case nil is returned."
535 (let* ((upper (car interval))
536 (freq (ical:recur-freq recur-value))
537 (unit (cl-case freq
538 (YEARLY :year)
539 (MONTHLY :month)
540 (WEEKLY :week)
541 (DAILY :day)
542 (HOURLY :hour)
543 (MINUTELY :minute)
544 (SECONDLY :second)))
545 (intervalsize (ical:recur-interval-size recur-value))
546 (new-low (ical:date/time-add upper unit (* -1 intervalsize) vtimezone))
547 (new-high (ical:date/time-add new-low unit 1 vtimezone)))
548
549 (when vtimezone
550 ;; (icr:tz-set-zone new-low vtimezone)
551 ;; (icr:tz-set-zone new-high vtimezone)
552 (icr:tz-set-zone upper vtimezone))
553
554 (unless (ical:date-time< new-high dtstart)
555 (list new-low new-high upper))))
556
557
558
559;; Refining intervals into subintervals
560;;
561;; For a given interval, the various BY*=... clauses in a recurrence
562;; rule specify the recurrences in that interval.
563;;
564;; RFC5545 unfortunately has an overly-complicated conceptual model for
565;; how recurrences are to be calculated which is based on "expanding" or
566;; "limiting" the recurrence set for each successive clause. This model
567;; is difficult to think about and implement, and the text of the
568;; standard is ambiguous. I did not succeed in producing a working
569;; implementation based on the description in the standard, and the
570;; existing implementations don't seem to agree on how it's to be
571;; implemented anyway.
572;;
573;; Fortunately, RFC8984 (JSCalendar) is a forthcoming standard which
574;; attempts to resolve the ambiguities while being semantically
575;; backward-compatible with RFC5545. It provides a much cleaner
576;; conceptual model: the recurrence set is generated by starting with a
577;; list of candidates, which consist of every second in (what is here
578;; called) an interval, and then filtering out any candidates which do
579;; not match the rule's clauses. The most straightforward implementation
580;; of this model, however, is unusably slow in typical cases. Consider
581;; for example the case of calculating the onset of daylight savings
582;; time in a given year: the interval is a year long, so it consists of
583;; over 31 million seconds. Although it's easy to generate Lisp
584;; timestamps for each of those seconds, filtering them through the
585;; various BY* clauses means decoding each of those timestamps, which
586;; means doing a fairly expensive computation over 31 million times, and
587;; then throwing away the result in all but one case. When I implemented
588;; this model, I was not patient enough to sit through the calculations
589;; for even MONTHLY rules (which on my laptop took minutes).
590;;
591;; So instead of implementing RFC8984's model directly, the strategy
592;; here is to do something equivalent but much more efficient: rather
593;; than thinking of an interval as consisting of a set of successive
594;; seconds, we think of it as described by its bounds; and for each BY*
595;; clause, we *refine* the interval into subintervals by computing the
596;; bounds of each subinterval corresponding to the value(s) in that
597;; clause. For example, in a YEARLY rule, the initial interval is one
598;; year long, say all of 2025. If it has a "BYMONTH=4,10" clause, then
599;; we refine this interval into two subintervals, each one month long:
600;; one for April 2025 and one for October 2025. This is much more
601;; efficient in the typical case, because the number of bounds which
602;; describe the final set of subintervals is usually *much* smaller than
603;; the number of seconds in the original interval.
604;;
605;; The following functions are responsible for computing these
606;; refinements. The main entry point here is
607;; `icalendar-recur-refine-from-clauses', which takes care of
608;; successively refining the interval both by the explicit values in the
609;; rule's clauses and by the implicit values in DTSTART. (There, too,
610;; RFC8984 is helpful: it gives a much more explicit description of how
611;; the information in DTSTART interacts with the BY* clauses to further
612;; refine the subintervals.)
613
614(defun icr:refine-byyearday (interval yeardays &optional vtimezone)
615 "Resolve INTERVAL into a list of subintervals matching YEARDAYS.
616
617YEARDAYS should be a list of values from a recurrence rule's
618BYYEARDAY=... clause; see `icalendar-recur' for the possible values."
619 (let* ((sorted-ydays (sort yeardays
620 :lessp (lambda (a b)
621 (let ((pos-a (if (< 0 a) a (+ 366 a)))
622 (pos-b (if (< 0 b) b (+ 366 b))))
623 (< pos-a pos-b)))))
624 (interval-start (car interval))
625 (start-year (decoded-time-year interval-start))
626 (interval-end (cadr interval))
627 (end-year (decoded-time-year interval-end))
628 (subintervals nil))
629 (while (<= start-year end-year)
630 ;; For each year in the interval...
631 (dolist (n sorted-ydays)
632 ;; ...the subinterval is one day long on the nth yearday
633 (let* ((nthday (calendar-date-from-day-of-year start-year n))
634 (low (ical:make-date-time :year start-year
635 :month (calendar-extract-month nthday)
636 :day (calendar-extract-day nthday)
637 :hour 0 :minute 0 :second 0
638 :tz vtimezone))
639 (high (ical:date/time-add low :day 1 vtimezone)))
640 ;; "Clip" the subinterval bounds if they fall outside the
641 ;; interval. Careful! This clipping can lead to high <= low,
642 ;; so need to check it is still the case that low < high
643 ;; before pushing the subinterval
644 (when (ical:date/time< low interval-start)
645 (setq low interval-start))
646 (when (ical:date/time< interval-end high)
647 (setq high interval-end))
648 (when (and (ical:date-time<= interval-start low)
649 (ical:date-time< low high)
650 (ical:date-time<= high interval-end))
651 (push (list low high) subintervals))))
652
653 (setq start-year (1+ start-year)))
654 (nreverse subintervals)))
655
656(defun icr:refine-byweekno (interval weeknos &optional weekstart vtimezone)
657 "Resolve INTERVAL into a list of subintervals matching WEEKNOS.
658
659WEEKNOS should be a list of values from a recurrence rule's
660BYWEEKNO=... clause, and WEEKSTART should be the value of its
661WKST=... clause (if any). See `icalendar-recur' for the possible values."
662 (let* ((sorted-weeknos (sort weeknos
663 :lessp (lambda (a b)
664 (let ((pos-a (if (< 0 a) a (+ 53 a)))
665 (pos-b (if (< 0 b) b (+ 53 b))))
666 (< pos-a pos-b)))))
667 (interval-start (car interval))
668 (start-year (decoded-time-year interval-start))
669 (interval-end (cadr interval))
670 (end-year (decoded-time-year interval-end))
671 (subintervals nil))
672 (while (<= start-year end-year)
673 ;; For each year in the interval...
674 (dolist (wn sorted-weeknos)
675 ;; ...the subinterval is one week long in the wn-th week
676 (let* ((nth-wstart (ical:start-of-weekno wn start-year weekstart))
677 (low (ical:make-date-time :year (calendar-extract-year nth-wstart)
678 :month (calendar-extract-month nth-wstart)
679 :day (calendar-extract-day nth-wstart)
680 :hour 0 :minute 0 :second 0
681 :tz vtimezone))
682 (high (ical:date/time-add low :day 7 vtimezone)))
683 ;; "Clip" the subinterval bounds if they fall outside the
684 ;; interval, as above. This can happen often here because week
685 ;; boundaries generally do not align with year boundaries.
686 (when (ical:date/time< low interval-start)
687 (setq low interval-start))
688 (when (ical:date/time< interval-end high)
689 (setq high interval-end))
690 (when (and (ical:date-time<= interval-start low)
691 (ical:date-time< low high)
692 (ical:date-time<= high interval-end))
693 (push (list low high) subintervals))))
694 (setq start-year (1+ start-year)))
695 (nreverse subintervals)))
696
697(defun icr:refine-bymonth (interval months &optional vtimezone)
698 "Resolve INTERVAL into a list of subintervals matching MONTHS.
699
700MONTHS should be a list of values from a recurrence rule's
701BYMONTH=... clause; see `icalendar-recur' for the possible values."
702 (let* ((sorted-months (sort months))
703 (interval-start (car interval))
704 (start-year (decoded-time-year interval-start))
705 (interval-end (cadr interval))
706 (end-year (decoded-time-year interval-end))
707 (subintervals nil))
708 (while (<= start-year end-year)
709 ;; For each year in the interval...
710 (dolist (m sorted-months)
711 ;; ...the subinterval is from the first day of the given month
712 ;; to the first day of the next
713 (let* ((low (ical:make-date-time :year start-year :month m :day 1
714 :hour 0 :minute 0 :second 0
715 :tz vtimezone))
716 (high (ical:date/time-add low :month 1 vtimezone)))
717
718 ;; Clip the subinterval bounds, as above
719 (when (ical:date/time< low interval-start)
720 (setq low interval-start))
721 (when (ical:date/time< interval-end high)
722 (setq high interval-end))
723 (when (and (ical:date/time<= interval-start low)
724 (ical:date/time< low high)
725 (ical:date/time<= high interval-end))
726 (push (list low high) subintervals))))
727 (setq start-year (1+ start-year)))
728
729 (nreverse subintervals)))
730
731(defun icr:refine-bymonthday (interval monthdays &optional vtimezone)
732 "Resolve INTERVAL into a list of subintervals matching MONTHDAYS.
733
734MONTHDAYS should be a list of values from a recurrence rule's
735BYMONTHDAY=... clause; see `icalendar-recur' for the possible values."
736 (let* ((sorted-mdays (sort monthdays
737 :lessp (lambda (a b)
738 (let ((pos-a (if (< 0 a) a (+ 31 a)))
739 (pos-b (if (< 0 b) b (+ 31 b))))
740 (< pos-a pos-b)))))
741 (interval-start (car interval))
742 (interval-end (cadr interval))
743 (subintervals nil))
744 (while (ical:date-time<= interval-start interval-end)
745 ;; For each month in the interval...
746 (dolist (m sorted-mdays)
747 ;; ...the subinterval is one day long on the given monthday
748 (let* ((month (ical:date/time-month interval-start))
749 (year (ical:date/time-year interval-start))
750 (monthday (if (< 0 m) m
751 (+ m 1 (calendar-last-day-of-month month year))))
752 (low (ical:date-time-variant interval-start :day monthday
753 :hour 0 :minute 0 :second 0
754 :tz vtimezone))
755 (high (ical:date/time-add low :day 1 vtimezone)))
756
757 (ignore-errors ; ignore invalid dates, e.g. 2025-02-29
758 ;; Clip subinterval, as above
759 (when (ical:date/time< low interval-start)
760 (setq low interval-start))
761 (when (ical:date/time< interval-end high)
762 (setq high interval-end))
763 (when (and (ical:date/time<= interval-start low)
764 (ical:date/time< low high)
765 (ical:date/time<= high interval-end))
766 (push (list low high) subintervals)))))
767 (setq interval-start
768 (ical:date/time-add interval-start :month 1 vtimezone)))
769 (nreverse subintervals)))
770
771(defun icr:refine-byday (interval weekdays &optional in-month vtimezone)
772 "Refine INTERVAL to days matching the given WEEKDAYS.
773
774WEEKDAYS should be a list of values from a recurrence rule's
775BYDAY=... clause; see `icalendar-recur' for the possible values.
776
777If WEEKDAYS contains pairs (DOW . OFFSET), then IN-MONTH indicates
778whether OFFSET is relative to the month of the start of the interval. If
779it is nil, OFFSET will be relative to the year, rather than the month."
780 (let* ((sorted-weekdays (sort (seq-filter #'natnump weekdays)))
781 (with-offsets (sort (seq-filter #'consp weekdays)
782 :lessp (lambda (w1 w2) (and (< (car w1) (car w2))))))
783 (interval-start (car interval))
784 (start-abs (calendar-absolute-from-gregorian
785 (ical:date-time-to-date interval-start)))
786 (interval-end (cadr interval))
787 (end-abs (calendar-absolute-from-gregorian
788 (ical:date-time-to-date interval-end)))
789 (subintervals nil))
790
791 ;; For days where an offset was given, the subinterval is a single
792 ;; weekday relative to the month or year of interval-start:
793 (dolist (wo with-offsets)
794 (let* ((dow (car wo))
795 (offset (cdr wo))
796 (low-date
797 (ical:nth-weekday-in offset dow
798 (ical:date/time-year interval-start)
799 (when in-month
800 (ical:date/time-month interval-start))))
801 (low (ical:date-to-date-time low-date :tz vtimezone))
802 (high (ical:date/time-add low :day 1 vtimezone)))
803 (when (ical:date/time< low interval-start)
804 (setq low interval-start))
805 (when (ical:date/time< interval-end high)
806 (setq high interval-end))
807 (when vtimezone
808 (icr:tz-set-zone low vtimezone)
809 (icr:tz-set-zone high vtimezone))
810 (when (and (ical:date/time<= interval-start low)
811 (ical:date/time<= high interval-end)
812 (ical:date/time< low high))
813 (push (list low high) subintervals))))
814
815 ;; When no offset was given, for each day in the interval...
816 (while (and (<= start-abs end-abs)
817 sorted-weekdays)
818 ;; ...the subinterval is one day long on matching weekdays.
819 (let* ((gdate (calendar-gregorian-from-absolute start-abs)))
820 (when (memq (calendar-day-of-week gdate) sorted-weekdays)
821 (let* ((low (ical:date-to-date-time gdate))
822 (high (ical:date/time-add low :day 1 vtimezone)))
823 (when (ical:date/time< low interval-start)
824 (setq low interval-start))
825 (when (ical:date/time< interval-end high)
826 (setq high interval-end))
827 (when vtimezone
828 (icr:tz-set-zone low vtimezone)
829 (icr:tz-set-zone high vtimezone))
830 (when (and (ical:date/time<= interval-start low)
831 (ical:date/time<= high interval-end)
832 (ical:date/time< low high))
833 (push (list low high) subintervals)))))
834 (setq start-abs (1+ start-abs)))
835
836 ;; Finally, sort and return all subintervals:
837 (sort subintervals
838 :lessp (lambda (int1 int2)
839 (ical:date-time< (car int1) (car int2)))
840 :in-place t)))
841
842(defun icr:refine-byhour (interval hours &optional vtimezone)
843 "Resolve INTERVAL into a list of subintervals matching HOURS.
844
845HOURS should be a list of values from a recurrence rule's
846BYHOUR=... clause; see `icalendar-recur' for the possible values."
847 (let* ((sorted-hours (sort hours))
848 (interval-start (car interval))
849 (interval-end (cadr interval))
850 (subintervals nil))
851 (while (ical:date-time<= interval-start interval-end)
852 ;; For each day in the interval...
853 (dolist (h sorted-hours)
854 ;; ...the subinterval is one hour long in the given hour
855 (let* ((low (ical:date-time-variant interval-start
856 :hour h :minute 0 :second 0
857 :tz vtimezone))
858 (high (ical:date/time-add low :hour 1 vtimezone)))
859 (ignore-errors ; do not generate subintervals for nonexisting times
860 (when (ical:date/time< low interval-start)
861 (setq low interval-start))
862 (when (ical:date/time< interval-end high)
863 (setq high interval-end))
864 (when (and (ical:date/time<= interval-start low)
865 (ical:date/time< low high)
866 (ical:date/time<= high interval-end))
867 (push (list low high) subintervals)))))
868 (setq interval-start (ical:date/time-add interval-start :day 1 vtimezone)))
869 (nreverse subintervals)))
870
871(defun icr:refine-byminute (interval minutes &optional vtimezone)
872 "Resolve INTERVAL into a list of subintervals matching MINUTES.
873
874MINUTES should be a list of values from a recurrence rule's
875BYMINUTE=... clause; see `icalendar-recur' for the possible values."
876 (let* ((sorted-minutes (sort minutes))
877 (interval-start (car interval))
878 (interval-end (cadr interval))
879 ;; we use absolute times (in seconds) for the loop variables in
880 ;; case the interval crosses the boundary between two observances:
881 (low-ts (time-convert (encode-time interval-start) 'integer))
882 (end-ts (time-convert (encode-time interval-end) 'integer))
883 (subintervals nil))
884 (while (<= low-ts end-ts)
885 ;; For each hour in the interval...
886 (dolist (m sorted-minutes)
887 ;; ...the subinterval is one minute long in the given minute
888 (let* ((low (ical:date-time-variant interval-start :minute m :second 0
889 :tz vtimezone))
890 (high (ical:date/time-add low :minute 1 vtimezone)))
891 (ignore-errors ; do not generate subintervals for nonexisting times
892 ;; Clip the subinterval, as above
893 (when (ical:date/time< low interval-start)
894 (setq low interval-start))
895 (when (ical:date/time< interval-end high)
896 (setq high interval-end))
897 (when (and (ical:date/time<= interval-start low)
898 (ical:date/time< low high)
899 (ical:date/time<= high interval-end))
900 (push (list low high) subintervals)))))
901 (setq low-ts (+ low-ts (* 60 60))
902 interval-start (if vtimezone (icr:tz-decode-time low-ts vtimezone)
903 (ical:date/time-add interval-start :hour 1))))
904 (nreverse subintervals)))
905
906(defun icr:refine-bysecond (interval seconds &optional vtimezone)
907 "Resolve INTERVAL into a list of subintervals matching SECONDS.
908
909SECONDS should be a list of values from a recurrence rule's
910BYSECOND=... clause; see `icalendar-recur' for the possible values."
911 (let* ((sorted-seconds (sort seconds))
912 (interval-start (car interval))
913 (interval-end (cadr interval))
914 ;; we use absolute times (in seconds) for the loop variables in
915 ;; case the interval crosses the boundary between two observances:
916 (low-ts (time-convert (encode-time interval-start) 'integer))
917 (end-ts (time-convert (encode-time interval-end) 'integer))
918 (subintervals nil))
919 (while (<= low-ts end-ts)
920 ;; For each minute in the interval...
921 (dolist (s sorted-seconds)
922 ;; ...the subinterval is one second long: the given second
923 (let* ((low (ical:date-time-variant interval-start :second s
924 :tz vtimezone))
925 (high (ical:date/time-add low :second 1 vtimezone)))
926 (when (ical:date/time< low interval-start)
927 (setq low interval-start))
928 (when (ical:date/time< interval-end high)
929 (setq high interval-end))
930 (when (and (ical:date/time<= interval-start low)
931 (ical:date/time< low high)
932 (ical:date/time<= high interval-end))
933 (push (list low high) subintervals))))
934 (setq low-ts (+ low-ts 60)
935 interval-start (if vtimezone
936 (icr:tz-decode-time low-ts vtimezone)
937 (ical:date/time-add interval-start :minute 1))))
938 (nreverse subintervals)))
939
940;; TODO: should this just become a generic function, with the above
941;; refine-by* functions becoming its methods?
942(defun icr:refine-by (unit interval values
943 &optional byday-inmonth weekstart vtimezone)
944 "Resolve INTERVAL into a list of subintervals matching VALUES for UNIT."
945 (cl-case unit
946 (BYYEARDAY (icr:refine-byyearday interval values vtimezone))
947 (BYWEEKNO (icr:refine-byweekno interval values weekstart vtimezone))
948 (BYMONTH (icr:refine-bymonth interval values vtimezone))
949 (BYMONTHDAY (icr:refine-bymonthday interval values vtimezone))
950 (BYDAY (icr:refine-byday interval values byday-inmonth vtimezone))
951 (BYHOUR (icr:refine-byhour interval values vtimezone))
952 (BYMINUTE (icr:refine-byminute interval values vtimezone))
953 (BYSECOND (icr:refine-bysecond interval values vtimezone))))
954
955(defun icr:make-bysetpos-filter (setpos)
956 "Return a filter on values for the indices in SETPOS.
957
958SETPOS should be a list of positive or negative integers between -366
959and 366, indicating a fixed index in a set of recurrences for *one
960interval* of a recurrence set, as found in the BYSETPOS=... clause of
961an `icalendar-recur'. For example, in a YEARLY recurrence rule with an
962INTERVAL of 1, the SETPOS represent indices in the recurrence instances
963generated for a single year.
964
965The returned value is a closure which can be called on the list of
966recurrences for one interval to filter it by index."
967 (lambda (dts)
968 (let* ((len (length dts))
969 (keep-indices (mapcar
970 (lambda (pos)
971 ;; sequence indices are 0-based, POS's are 1-based:
972 (if (< pos 0)
973 (+ pos len)
974 (1- pos)))
975 setpos)))
976 (delq nil
977 (seq-map-indexed
978 (lambda (dt index)
979 (when (memq index keep-indices)
980 dt))
981 dts)))))
982
983(defun icr:refine-from-clauses (interval recur-value dtstart
984 &optional vtimezone)
985 "Resolve INTERVAL into subintervals based on the clauses in RECUR-VALUE.
986
987The resulting list of subintervals represents all times in INTERVAL
988which match the BY* clauses of RECUR-VALUE except BYSETPOS, as well as
989the constraints implicit in DTSTART. (For example, if there is no
990BYMINUTE clause, subintervals will have the same minute value as
991DTSTART.)
992
993If specified, VTIMEZONES should be a list of `icalendar-vtimezone'
994components and TZID should be the `icalendar-tzid' property value of one
995of those timezones. In this case, TZID states the time zone of DTSTART,
996and the offsets effective in that time zone on the dates and times of
997recurrences will be local to that time zone."
998 (let ((freq (ical:recur-freq recur-value))
999 (weekstart (ical:recur-weekstart recur-value))
1000 (subintervals (list interval)))
1001
1002 (dolist (byunit (list 'BYMONTH 'BYWEEKNO
1003 'BYYEARDAY 'BYMONTHDAY 'BYDAY
1004 'BYHOUR 'BYMINUTE 'BYSECOND))
1005 (let ((values (ical:recur-by* byunit recur-value))
1006 (in-month nil))
1007 ;; When there is no explicit BY* clause, use the value implicit
1008 ;; in DTSTART. (These conditions are adapted from RFC8984:
1009 ;; https://www.rfc-editor.org/rfc/rfc8984.html#section-4.3.3.1-4.3.1
1010 ;; Basically, the conditions are somewhat complicated because
1011 ;; the meanings of various BY* clauses are not independent and
1012 ;; so we have to be careful about the information we take to be
1013 ;; implicit in DTSTART, especially with MONTHLY and YEARLY
1014 ;; rules. For example, we *do* want to take the weekday of
1015 ;; DTSTART as an implicit constraint if a BYWEEKNO clause is
1016 ;; present, but not if an explicit BYDAY or BYMONTHDAY clause is
1017 ;; also present, since they might contain conflicting
1018 ;; constraints.)
1019 (when (and (eq byunit 'BYSECOND)
1020 (not (eq freq 'SECONDLY))
1021 (not values))
1022 (setq values (list (ical:date/time-second dtstart))))
1023 (when (and (eq byunit 'BYMINUTE)
1024 (not (memq freq '(SECONDLY MINUTELY)))
1025 (not values))
1026 (setq values (list (ical:date/time-minute dtstart))))
1027 (when (and (eq byunit 'BYHOUR)
1028 (not (memq freq '(SECONDLY MINUTELY HOURLY)))
1029 (not values))
1030 (setq values (list (ical:date/time-hour dtstart))))
1031 (when (and (eq byunit 'BYDAY)
1032 (eq freq 'WEEKLY)
1033 (not values))
1034 (setq values (list (ical:date/time-weekday dtstart))))
1035 (when (and (eq byunit 'BYMONTHDAY)
1036 (eq freq 'MONTHLY)
1037 (not (ical:recur-by* 'BYDAY recur-value))
1038 (not values))
1039 (setq values (list (ical:date/time-monthday dtstart))))
1040 (when (and (eq freq 'YEARLY)
1041 (not (ical:recur-by* 'BYYEARDAY recur-value)))
1042 (when (and (eq byunit 'BYMONTH)
1043 (not values)
1044 (not (ical:recur-by* 'BYWEEKNO recur-value))
1045 (or (ical:recur-by* 'BYMONTHDAY recur-value)
1046 (not (ical:recur-by* 'BYDAY recur-value))))
1047 (setq values (list (ical:date/time-month dtstart))))
1048 (when (and (eq byunit 'BYMONTHDAY)
1049 (not values)
1050 (not (ical:recur-by* 'BYWEEKNO recur-value))
1051 (not (ical:recur-by* 'BYDAY recur-value)))
1052 (setq values (list (ical:date/time-monthday dtstart))))
1053 (when (and (eq byunit 'BYDAY)
1054 (not values)
1055 (ical:recur-by* 'BYWEEKNO recur-value)
1056 (not (ical:recur-by* 'BYMONTHDAY recur-value)))
1057 (setq values (list (ical:date/time-weekday dtstart)))))
1058
1059 ;; Handle offsets in a BYDAY clause:
1060 ;; "If present, this [offset] indicates the nth occurrence of a
1061 ;; specific day within the MONTHLY or YEARLY "RRULE". For
1062 ;; example, within a MONTHLY rule, +1MO (or simply 1MO)
1063 ;; represents the first Monday within the month, whereas -1MO
1064 ;; represents the last Monday of the month. The numeric value
1065 ;; in a BYDAY rule part with the FREQ rule part set to YEARLY
1066 ;; corresponds to an offset within the month when the BYMONTH
1067 ;; rule part is present"
1068 (when (and (eq byunit 'BYDAY)
1069 (or (eq freq 'MONTHLY)
1070 (and (eq freq 'YEARLY)
1071 (ical:recur-by* 'BYMONTH recur-value))))
1072 (setq in-month t))
1073
1074 ;; On each iteration of the loop, we refine the subintervals
1075 ;; with these explicit or implicit values:
1076 (when values
1077 (setq subintervals
1078 (delq nil
1079 (mapcan (lambda (in)
1080 (icr:refine-by byunit in values in-month
1081 weekstart vtimezone))
1082 subintervals))))))
1083
1084 ;; Finally return the refined subintervals after we've looked at all
1085 ;; clauses:
1086 subintervals))
1087
1088;; Once we have refined an interval into a final set of subintervals, we
1089;; need to convert those subintervals into a set of recurrences. For a
1090;; recurrence set where DTSTART and the recurrences are date-times, the
1091;; recurrence set (in this interval) consists of every date-time
1092;; corresponding to each second of any subinterval. When DTSTART and the
1093;; recurrences are plain dates, the recurrence set consists of each
1094;; distinct date in any subinterval.
1095(defun icr:subintervals-to-date-times (subintervals &optional vtimezone)
1096 "Transform SUBINTERVALS into a list of `icalendar-date-time' recurrences.
1097
1098The returned list of recurrences contains one date-time value for each
1099second of each subinterval."
1100 (let (recurrences)
1101 (dolist (int subintervals)
1102 (let* ((start (car int))
1103 (dt start)
1104 ;; Use absolute times for the loop in case the subinterval
1105 ;; crosses the boundary between two observances.
1106 ;; N.B. floating times will be correctly treated as local
1107 ;; times by encode-time.
1108 (end (time-convert (encode-time (cadr int)) 'integer))
1109 (tick (time-convert (encode-time start) 'integer)))
1110 (while (time-less-p tick end)
1111 (push dt recurrences)
1112 (setq tick (1+ tick)
1113 dt (if vtimezone (icr:tz-decode-time tick vtimezone)
1114 (ical:date/time-add dt :second 1))))))
1115 (nreverse recurrences)))
1116
1117(defun icr:subintervals-to-dates (subintervals)
1118 "Transform SUBINTERVALS into a list of `icalendar-date' recurrences.
1119
1120The returned list of recurrences contains one date value for each
1121day of each subinterval."
1122 (let (recurrences)
1123 (dolist (int subintervals)
1124 (let* ((start (car int))
1125 (start-abs (calendar-absolute-from-gregorian
1126 (ical:date-time-to-date start)))
1127 (end (cadr int))
1128 (end-abs (calendar-absolute-from-gregorian
1129 (ical:date-time-to-date end)))
1130 ;; end is an exclusive upper bound, but number-sequence
1131 ;; needs an *inclusive* upper bound, so if end is at
1132 ;; midnight, the bound is the previous day:
1133 (bound (if (zerop (+ (decoded-time-hour end)
1134 (decoded-time-minute end)
1135 (decoded-time-second end)))
1136 (1- end-abs)
1137 end-abs)))
1138 (setq recurrences
1139 (append recurrences
1140 (mapcar #'calendar-gregorian-from-absolute
1141 (number-sequence start-abs bound))))))
1142 recurrences))
1143
1144(defun icr:subintervals-to-recurrences (subintervals dtstart &optional vtimezone)
1145 "Transform SUBINTERVALS into a list of recurrences.
1146
1147The returned list of recurrences contains all distinct values in each
1148subinterval of the same type as DTSTART."
1149 (if (cl-typep dtstart 'ical:date)
1150 (icr:subintervals-to-dates subintervals)
1151 (icr:subintervals-to-date-times subintervals vtimezone)))
1152
1153
1154;; Calculating recurrences in a given interval or window
1155;;
1156;; We can now put all of the above together to compute the set of
1157;; recurrences in a given interval (`icr:recurrences-in-interval'), and
1158;; thereby in a given window (`icr:recurences-in-window'); or, if the
1159;; rule describing the set has a COUNT clause, we can enumerate the
1160;; recurrences in each interval starting from the beginning of the set
1161;; (`icr:recurrences-to-count').
1162(defun icr:recurrences-in-interval (interval component &optional vtimezone nmax)
1163 "Return a list of the recurrences of COMPONENT in INTERVAL.
1164
1165INTERVAL should be a list (LOW HIGH NEXT) of date-times which bound a
1166single recurrence interval, as returned e.g. by
1167`icalendar-recur-find-interval'. (To find the recurrences in an
1168arbitrary window of time, rather than between interval boundaries, see
1169`icalendar-recur-recurrences-in-window'.)
1170
1171COMPONENT should be an iCalendar component node representing a recurring
1172event: it should contain at least an `icalendar-dtstart' and either an
1173`icalendar-rrule' or `icalendar-rdate' property.
1174
1175If specified, VTIMEZONE should be an `icalendar-vtimezone' component.
1176In this case, the dates and times of recurrences will be computed with
1177UTC offsets local to that time zone.
1178
1179If specified, NMAX should be a positive integer containing a maximum
1180number of recurrences to return from this interval. In this case, if the
1181interval contains more than NMAX recurrences, only the first NMAX
1182recurrences will be returned; otherwise all recurrences in the interval
1183are returned. (The NMAX argument mainly exists to support recurrence
1184rules with a COUNT clause; see `icalendar-recur-recurrences-to-count'.)
1185
1186The returned list is a list of `icalendar-date' or `icalendar-date-time'
1187values representing the start times of recurrences. Note that any
1188values of type `icalendar-period' in COMPONENT's `icalendar-rdate'
1189property (or properties) will NOT be included in the list; it is the
1190callee's responsibility to handle any such values separately.
1191
1192The computed recurrences for INTERVAL are cached in COMPONENT and
1193retrieved on subsequent calls with the same arguments."
1194 (ical:with-component component
1195 ((ical:dtstart :value dtstart)
1196 (ical:tzoffsetfrom :value offset-from)
1197 (ical:rrule :value recur-value)
1198 (ical:rdate :all rdate-nodes) ;; TODO: these can also be ical:period values
1199 (ical:exdate :all exdate-nodes))
1200 (if (not (or recur-value rdate-nodes))
1201 ;; No recurrences to calculate, so just return early:
1202 nil
1203 ;; Otherwise, calculate recurrences in the interval:
1204 (when (memq (ical:ast-node-type component) '(ical:standard ical:daylight))
1205 ;; In time zone observances, set the zone field in dtstart
1206 ;; from the TZOFFSETFROM property:
1207 (setq dtstart
1208 (ical:date-time-variant dtstart
1209 :zone offset-from
1210 :dst (not (ical:daylight-component-p
1211 component)))))
1212 (cl-labels ((get-interval
1213 (apply-partially #'icr:-set-get-interval component))
1214 (put-interval
1215 (apply-partially #'icr:-set-put-interval component)))
1216 (let ((cached (get-interval interval)))
1217 (cond ((eq cached :none) nil)
1218 (cached cached)
1219 (t
1220 (let* (;; Start by generating all the recurrences matching the
1221 ;; BY* clauses except for BYSETPOS:
1222 (subs (icr:refine-from-clauses interval recur-value dtstart
1223 vtimezone))
1224 (sub-recs (icr:subintervals-to-recurrences subs dtstart
1225 vtimezone))
1226 ;; Apply any BYSETPOS clause to this set:
1227 (keep-indices (ical:recur-by* 'BYSETPOS recur-value))
1228 (pos-recs
1229 (if keep-indices
1230 (funcall (icr:make-bysetpos-filter keep-indices)
1231 sub-recs)
1232 sub-recs))
1233 ;; Remove any recurrences before DTSTART or after UNTIL
1234 ;; (both of which are inclusive bounds):
1235 (until (ical:recur-until recur-value))
1236 (until-recs
1237 (seq-filter
1238 (lambda (rec) (and (ical:date/time<= dtstart rec)
1239 (or (not until)
1240 (ical:date/time<= rec until))))
1241 pos-recs))
1242 ;; Include any values in the interval from the
1243 ;; RDATE property:
1244 (low (car interval))
1245 (high (cadr interval))
1246 (rdates
1247 (mapcar #'ical:ast-node-value
1248 (apply #'append
1249 (mapcar #'ical:ast-node-value
1250 rdate-nodes))))
1251 (interval-rdates
1252 (seq-filter
1253 (lambda (rec)
1254 ;; only include ical:date and ical:date-time
1255 ;; values from RDATE; callee is responsible
1256 ;; for handling ical:period values
1257 (unless (cl-typep rec 'ical:period)
1258 (and (ical:date/time<= low rec)
1259 (ical:date/time< high rec))))
1260 rdates))
1261 (included-recs (append until-recs interval-rdates))
1262 ;; Exclude any values from the EXDATE property;
1263 ;; this gives us the complete set of recurrences
1264 ;; in this interval:
1265 (exdates
1266 (mapcar #'ical:ast-node-value
1267 (append
1268 (mapcar #'ical:ast-node-value exdate-nodes))))
1269 (all-recs
1270 (if exdates
1271 (seq-filter
1272 (lambda (rec) (not (member rec exdates)))
1273 included-recs)
1274 included-recs))
1275 ;; Limit to the first NMAX recurrences if requested.
1276 ;; `icr:recurrences-to-count' provides NMAX so as not to
1277 ;; store more recurrences in the final interval than the
1278 ;; COUNT clause allows:
1279 (nmax-recs
1280 (if nmax (seq-take all-recs nmax)
1281 all-recs)))
1282 ;; Store and return the computed recurrences:
1283 (put-interval interval (or nmax-recs :none))
1284 nmax-recs))))))))
1285
1286(defun icr:recurrences-in-window (lower upper component &optional vtimezone)
1287 "Return the recurrences of COMPONENT in the window between LOWER and UPPER.
1288
1289LOWER and UPPER may be arbitrary `icalendar-date' or
1290`icalendar-date-time' values. COMPONENT should be an iCalendar component
1291node representing a recurring event: it should contain at least an
1292`icalendar-dtstart' and either an `icalendar-rrule' or `icalendar-rdate'
1293property.
1294
1295If specified, VTIMEZONE should be an `icalendar-vtimezone' component.
1296In this case, the dates and times of recurrences will be computed with
1297UTC offsets local to that time zone."
1298 (ical:with-component component
1299 ((ical:dtstart :value dtstart)
1300 (ical:tzoffsetfrom :value offset-from)
1301 (ical:rrule :value recur-value)
1302 (ical:rdate :all rdate-nodes))
1303 (if (not (or recur-value rdate-nodes))
1304 ;; No recurrences to calculate, so just return early:
1305 nil
1306 ;; Otherwise, calculate the recurrences in the window:
1307 (when (memq (ical:ast-node-type component) '(ical:standard ical:daylight))
1308 ;; in time zone observances, set the zone field in dtstart
1309 ;; from the TZOFFSETFROM property:
1310 (setq dtstart
1311 (ical:date-time-variant dtstart
1312 :zone offset-from
1313 :dst (not (ical:daylight-component-p
1314 component)))))
1315
1316 (let* (;; don't look for nonexistent intervals:
1317 (low-start (if (ical:date/time< lower dtstart) dtstart lower))
1318 (until (ical:recur-until recur-value))
1319 (high-end (if (and until (ical:date/time< until upper)) until upper))
1320 (curr-interval (icr:find-interval low-start dtstart recur-value
1321 vtimezone))
1322 (high-interval (icr:find-interval high-end dtstart recur-value
1323 vtimezone))
1324 (high-intbound (cadr high-interval))
1325 (recurrences nil))
1326
1327 (while (ical:date-time< (car curr-interval) high-intbound)
1328 (setq recurrences
1329 (append
1330 (icr:recurrences-in-interval curr-interval component vtimezone)
1331 recurrences))
1332 (setq curr-interval (icr:next-interval curr-interval recur-value
1333 vtimezone)))
1334
1335 ;; exclude any recurrences inside the first and last intervals but
1336 ;; outside the window before returning:
1337 (seq-filter
1338 (lambda (dt)
1339 (and (ical:date/time<= lower dt)
1340 (ical:date/time< dt upper)))
1341 recurrences)))))
1342
1343(defun icr:recurrences-in-window-w/end-times
1344 (lower upper component &optional vtimezone)
1345 "Like `icalendar-recurrences-in-window', but returns end times.
1346
1347The return value is a list of (START END) pairs representing the start
1348and end time of each recurrence of COMPONENT in the window defined by
1349LOWER and UPPER.
1350
1351In the returned pairs, START and END are both `icalendar-date' or
1352`icalendar-date-time' values of the same type as COMPONENT's
1353`icalendar-dtstart'. Each END time is computed by adding COMPONENT's
1354`icalendar-duration' value to START for each recurrence START between
1355LOWER and UPPER. Or, if the recurrence is given by an `icalendar-period'
1356value in an `icalendar-rdate' property, START and END are determined by
1357the period."
1358 (ical:with-component component
1359 ((ical:duration :value duration)
1360 (ical:rdate :all rdate-nodes))
1361 ;; TODO: for higher-level applications showing a schedule, it might
1362 ;; be useful to include recurrences which start outside the window,
1363 ;; but end inside it. This would mean we can't simply use
1364 ;; `recurrences-in-window' like this.
1365 (let ((starts (icr:recurrences-in-window lower upper component vtimezone))
1366 (periods (seq-filter
1367 (lambda (vnode)
1368 (when (eq 'ical:period (ical:ast-node-type vnode))
1369 (ical:ast-node-value vnode)))
1370 (append
1371 (mapcar #'ical:ast-node-value rdate-nodes)))))
1372 (when (or starts periods)
1373 (seq-uniq
1374 (append (mapcar
1375 (lambda (dt) (list dt (ical:date/time-add-duration
1376 dt duration vtimezone)))
1377 starts)
1378 (mapcar
1379 (lambda (p)
1380 (let ((start (ical:period-start p)))
1381 (list start
1382 (or (ical:period-end p)
1383 (ical:date/time-add-duration
1384 start (ical:period-dur-value p) vtimezone)))))
1385 periods)))))))
1386
1387(defun icr:recurrences-to-count (component &optional vtimezone)
1388 "Return all the recurrences in COMPONENT up to COUNT in its recurrence rule.
1389
1390COMPONENT should be an iCalendar component node representing a recurring
1391event: it should contain at least an `icalendar-dtstart' and an
1392`icalendar-rrule', which must contain a COUNT=... clause.
1393
1394Warning: this function finds *all* the recurrences in COMPONENT's
1395recurrence set. If the value of COUNT is large, this can be slow.
1396
1397If specified, VTIMEZONE should be an `icalendar-vtimezone' component.
1398In this case, the dates and times of recurrences will be computed with
1399UTC offsets local to that time zone."
1400 (ical:with-component component
1401 ((ical:dtstart :value dtstart)
1402 (ical:tzoffsetfrom :value offset-from)
1403 (ical:rrule :value recur-value)
1404 (ical:rdate :all rdate-nodes))
1405 (when (memq (ical:ast-node-type component) '(ical:standard ical:daylight))
1406 ;; in time zone observances, set the zone field in dtstart
1407 ;; from the TZOFFSETFROM property:
1408 (setq dtstart
1409 (ical:date-time-variant dtstart
1410 :zone offset-from
1411 :dst (not (ical:daylight-component-p
1412 component)))))
1413 (unless (or recur-value rdate-nodes)
1414 (error "No recurrence data in component: %s" component))
1415 (unless (ical:recur-count recur-value)
1416 (error "Recurrence rule has no COUNT clause"))
1417 (let ((count (ical:recur-count recur-value))
1418 (int (icr:nth-interval 0 dtstart recur-value vtimezone))
1419 recs)
1420 (while (length< recs count)
1421 (setq recs
1422 (append recs (icr:recurrences-in-interval int component vtimezone
1423 (- count (length recs)))))
1424 (setq int (icr:next-interval int recur-value vtimezone)))
1425 recs)))
1426
1427
1428
1429;; Recurrence set representation
1430;;
1431;; We represent a recurrence set as a map from intervals to the
1432;; recurrences in that interval. The primary purpose of this
1433;; representation is to memoize the computation of recurrences, since
1434;; the computation is relatively expensive and the results are needed
1435;; repeatedly, particularly for time zone observances. The map is stored
1436;; in the `:recurrence-set' property of the iCalendar component which
1437;; represents the recurring event.
1438;;
1439;; Internally, we use a hash table for the map, since the set can grow
1440;; quite large. We use the start date-times of intervals as the keys,
1441;; since these uniquely identify intervals within a given component; we
1442;; ignore the weekday, zone and dst fields in the keys, mostly to avoid
1443;; cache misses during time zone observance lookups, which must generate
1444;; intervals with different zone values.
1445;;
1446;; In order to avoid repeating the computation of recurrences, we store
1447;; the keyword `:none' as the value when there are no recurrences in a
1448;; given interval. This distinguishes the value from nil, so that,
1449;; whereas (gethash some-key the-map) => nil means "We haven't computed
1450;; recurrences yet for this interval", (gethash some-key the-map) =>
1451;; :none means "We've computed that there are no recurrences in this
1452;; interval", and can skip the computation of recurrences. See
1453;; `icalendar-recur-recurrences-in-interval', which performs the check.
1454
1455(defun icr:-make-set ()
1456 (make-hash-table :test #'equal))
1457
1458(defsubst icr:-key-from-interval (interval)
1459 (take 6 (car interval))) ; (secs mins hours day month year)
1460
1461(defun icr:-set-get-interval (component interval)
1462 (let ((set (ical:ast-node-meta-get :recurrence-set component))
1463 (key (icr:-key-from-interval interval)))
1464 (when (hash-table-p set)
1465 (gethash key set))))
1466
1467(defun icr:-set-put-interval (component interval recurrences)
1468 (let ((set (or (ical:ast-node-meta-get :recurrence-set component)
1469 (icr:-make-set)))
1470 (key (icr:-key-from-interval interval)))
1471 (setf (gethash key set) recurrences)
1472 (ical:ast-node-meta-set component :recurrence-set set)))
1473
1474
1475;; Timezones:
1476
1477(define-error 'ical:tz-nonexistent-time "Date-time does not exist" 'ical:error)
1478
1479(define-error 'ical:tz-no-observance "No observance found for date-time"
1480 'ical:error)
1481
1482(define-error 'ical:tz-data-insufficient
1483 "Insufficient time zone data to create VTIMEZONE"
1484 'ical:error)
1485
1486(define-error 'ical:tz-unsupported
1487 "Time zone rules not expressible as iCalendar RRULE"
1488 'ical:error)
1489
1490;; In RFC5545 Section 3.3.10, we read: "If the computed local start time
1491;; of a recurrence instance does not exist ... the time of the
1492;; recurrence instance is interpreted in the same manner as an explicit
1493;; DATE-TIME value describing that date and time, as specified in
1494;; Section 3.3.5." which in turn says:
1495;; "If, based on the definition of the referenced time zone, the local
1496;; time described occurs more than once (when changing from daylight to
1497;; standard time), the DATE-TIME value refers to the first occurrence of
1498;; the referenced time. Thus, TZID=America/New_York:20071104T013000
1499;; indicates November 4, 2007 at 1:30 A.M. EDT (UTC-04:00). If the
1500;; local time described does not occur (when changing from standard to
1501;; daylight time), the DATE-TIME value is interpreted using the UTC
1502;; offset before the gap in local times. Thus,
1503;; TZID=America/New_York:20070311T023000 indicates March 11, 2007 at
1504;; 3:30 A.M. EDT (UTC-04:00), one hour after 1:30 A.M. EST (UTC-05:00)."
1505
1506;; TODO: verify that these functions are correct for time zones other
1507;; than US Eastern.
1508(defun icr:nonexistent-date-time-p (dt obs-onset observance)
1509 "Return non-nil if DT does not exist in a given OBSERVANCE.
1510
1511Some local date-times do not exist in a given time zone. When switching
1512from standard to daylight savings time, the local clock time jumps over
1513a certain range of times. This function tests whether DT is one of those
1514non-existent local times.
1515
1516DT and OBS-ONSET should be `icalendar-date-time' values; OBS-ONSET
1517should be the (local) time immediately at the onset of the
1518OBSERVANCE. OBSERVANCE should be an `icalendar-standard' or
1519`icalendar-daylight' component.
1520
1521If this function returns t, then per RFC5545 Section 3.3.5, DT must be
1522interpreted using the UTC offset in effect prior to the onset of
1523OBSERVANCE. For example, at the switch from Standard to Daylight
1524Savings time in US Eastern, the nonexistent time 2:30AM (Standard) must
1525be re-interpreted as 3:30AM DST."
1526 (when (ical:daylight-component-p observance)
1527 (ical:with-component observance
1528 ((ical:tzoffsetfrom :value offset-from)
1529 (ical:tzoffsetto :value offset-to))
1530 (and (= (decoded-time-year dt) (decoded-time-year obs-onset))
1531 (= (decoded-time-month dt) (decoded-time-month obs-onset))
1532 (= (decoded-time-day dt) (decoded-time-day obs-onset))
1533 (let* ((onset-secs (+ (decoded-time-second obs-onset)
1534 (* 60 (decoded-time-minute obs-onset))
1535 (* 60 60 (decoded-time-hour obs-onset))))
1536 (dt-secs (+ (decoded-time-second dt)
1537 (* 60 (decoded-time-minute dt))
1538 (* 60 60 (decoded-time-hour dt))))
1539 (jumped (abs (- offset-from offset-to)))
1540 (after-jumped (+ onset-secs jumped)))
1541 (and
1542 (<= onset-secs dt-secs)
1543 (< dt-secs after-jumped)))))))
1544
1545(defun icr:date-time-occurs-twice-p (dt obs-onset observance)
1546 "Return non-nil if DT occurs twice in the given OBSERVANCE.
1547
1548Some local date-times occur twice in a given time zone. When switching
1549from daylight savings to standard time time, the local clock time is
1550typically set back, so that a certain range of clock times occurs twice,
1551once in daylight savings time and once in standard time. This function
1552tests whether DT is one of those local times which occur twice.
1553
1554DT and OBS-ONSET should be `icalendar-date-time' values; OBS-ONSET
1555should be the (local) time immediately at the relevant onset of the
1556OBSERVANCE. OBSERVANCE should be an `icalendar-standard' or
1557`icalendar-daylight' component.
1558
1559If this function returns t, then per RFC5545 Section 3.3.5, DT must be
1560interpreted as the first occurrence of this clock time, i.e., in
1561daylight savings time, prior to OBS-ONSET."
1562 (when (ical:standard-component-p observance)
1563 (ical:with-component observance
1564 ((ical:tzoffsetfrom :value offset-from)
1565 (ical:tzoffsetto :value offset-to))
1566 (and (= (decoded-time-year dt) (decoded-time-year obs-onset))
1567 (= (decoded-time-month dt) (decoded-time-month obs-onset))
1568 (= (decoded-time-day dt) (decoded-time-day obs-onset))
1569 (let* ((onset-secs (+ (decoded-time-second obs-onset)
1570 (* 60 (decoded-time-minute obs-onset))
1571 (* 60 60 (decoded-time-hour obs-onset))))
1572 (dt-secs (+ (decoded-time-second dt)
1573 (* 60 (decoded-time-minute dt))
1574 (* 60 60 (decoded-time-hour dt))))
1575 (repeated (abs (- offset-from offset-to)))
1576 (start-repeateds (- onset-secs repeated)))
1577 (and
1578 (<= start-repeateds dt-secs)
1579 (< dt-secs onset-secs)))))))
1580
1581(defun icr:tz--get-updated-in (dt obs-onset observance)
1582 "Determine how to update DT's zone and dst slots from OBSERVANCE.
1583
1584DT should be an `icalendar-date-time', OBSERVANCE an
1585`icalendar-standard' or `icalendar-daylight', and OBS-ONSET the nearest
1586onset of OBSERVANCE before DT. Returns an `icalendar-date-time' that can
1587be used to update DT.
1588
1589In most cases, the return value will contain a zone offset equal to
1590OBSERVANCE's `icalendar-tzoffsetto' value.
1591
1592However, when DT falls within a range of nonexistent times after
1593OBS-ONSET, or a range of local times that occur twice (see
1594`icalendar-recur-nonexistent-date-time-p' and
1595`icalendar-recur-date-time-occurs-twice-p'), it needs to be interpreted
1596with the UTC offset in effect prior to the OBS-ONSET of OBSERVANCE (see
1597RFC5545 Section 3.3.5). So e.g. at the switch from Standard to Daylight
1598in US Eastern, 2:30AM EST (a nonexistent time) becomes 3:30AM EDT, and
1599at the switch from Daylight to Standard, 1:30AM (which occurs twice)
1600becomes 1:30AM EDT, the first occurence."
1601 (ical:with-component observance
1602 ((ical:tzoffsetfrom :value offset-from)
1603 (ical:tzoffsetto :value offset-to))
1604 (let* ((is-daylight (ical:daylight-component-p observance))
1605 (to-dt (ical:date-time-variant dt :dst is-daylight :zone offset-to))
1606 (from-dt (ical:date-time-variant dt :dst (not is-daylight)
1607 :zone offset-from))
1608 updated)
1609 (cond ((icr:nonexistent-date-time-p to-dt obs-onset observance)
1610 ;; In this case, RFC5545 requires that we take the same
1611 ;; point in absolute time as from-dt, but re-decode it into
1612 ;; to-dt's zone:
1613 (setq updated (decode-time (encode-time from-dt) offset-to))
1614 (setf (decoded-time-dst updated) is-daylight))
1615 ((icr:date-time-occurs-twice-p to-dt obs-onset observance)
1616 ;; In this case, RFC5545 requires that we interpret dt as
1617 ;; from-dt, since that is the first occurrence of the clock
1618 ;; time in the zone:
1619 (setq updated from-dt))
1620 (t
1621 ;; Otherwise we interpret dt as to-dt, i.e., with the
1622 ;; offset effective within the observance:
1623 (setq updated to-dt)))
1624 updated)))
1625
1626(defun icr:tz-for (tzid vtimezones)
1627 "Return the `icalendar-vtimezone' for the TZID.
1628
1629VTIMEZONES should be a list of `icalendar-vtimezone' components. TZID
1630should be a time zone identifier, as found e.g. in an
1631`icalendar-tzidparam' parameter. The first time zone in VTIMEZONES whose
1632`icalendar-tzid' value matches this parameter's value is returned."
1633 (catch 'found
1634 (dolist (tz vtimezones)
1635 (ical:with-component tz
1636 ((ical:tzid :value tzidval))
1637 (when (equal tzidval tzid)
1638 (throw 'found tz))))))
1639
1640;; DRAGONS DRAGONS DRAGONS
1641(defun icr:tz-observance-on (dt vtimezone &optional update nonexisting)
1642 "Return the time zone observance in effect on DT in VTIMEZONE.
1643
1644If there is such an observance, the returned value is a list (OBSERVANCE
1645ONSET). OBSERVANCE is an `icalendar-standard' or `icalendar-daylight'
1646component node. ONSET is the recurrence of OBSERVANCE (an
1647`icalendar-date-time') which occurs closest in time, but before, DT.
1648
1649If there is no such observance in VTIMEZONE, the returned value is nil.
1650
1651VTIMEZONE should be an `icalendar-vtimezone' component node.
1652
1653DT may be an an `icalendar-date-time' or a Lisp timestamp. If it is a
1654date-time, it represents a local time assumed to be in VTIMEZONE. Any
1655existing offset in DT is ignored, and DT is compared with the local
1656clock time at the start of each observance in VTIMEZONE to determine the
1657correct observance and onset. (This is so that the correct observance
1658can be found for clock times generated during recurrence rule
1659calculations.)
1660
1661If UPDATE is non-nil, the observance found will be used to update the
1662offset value in DT (as a side effect) before returning the observance
1663and onset.
1664
1665If UPDATE is non-nil, NONEXISTING specifies how to handle clock times
1666that do not exist in the observance (see
1667`icalendar-recur-tz-nonexistent-date-time-p'). The keyword `:error'
1668means to signal an \\='icalendar-tz-nonexistent-time error, without
1669modifying any of the fields in DT. Otherwise, the default is to
1670interpret DT using the offset from UTC before the onset of the found
1671observance, and then reset the clock time in DT to the corresponding
1672existing time after the onset of the observance. For example, the
1673nonexisting time 2:30AM in Standard time on the day of the switch to
1674Daylight time in the US Eastern time zone will be reset to 3:30AM
1675Eastern Daylight time.
1676
1677If DT is a Lisp timestamp, it represents an absolute time and
1678comparisons with the onsets in VTIMEZONE are performed with absolute
1679times. UPDATE and NONEXISTING have no meaning in this case and are
1680ignored."
1681 (ical:with-component vtimezone
1682 ((ical:standard :all stds)
1683 (ical:daylight :all dls))
1684 (let (given-abs-time ;; = `dt', if given a Lisp timestamp
1685 given-clock-time ;; = `dt', if given a decoded time
1686 nearest-observance ;; the observance we're looking for
1687 nearest-onset ;; latest onset of this observance before `dt'
1688 updated) ;; stores how `dt's fields should be updated
1689 ;; in line with this observance, if requested
1690
1691 (if (cl-typep dt 'ical:date-time)
1692 ;; We were passed a date-time with local clock time, not an
1693 ;; absolute time; in this case, we must make local clock time
1694 ;; comparisons with the observance onset start and recurrences
1695 ;; (in order to determine the correct offset for it within the
1696 ;; zone)
1697 (setq given-clock-time dt
1698 given-abs-time nil)
1699 ;; We were passed an absolute time, not a date-time; in this
1700 ;; case, we can make comparisons in absolute time with
1701 ;; observance onset start and recurrences (in order to determine
1702 ;; the correct offset for decoding it)
1703 (setq given-abs-time dt
1704 given-clock-time nil))
1705
1706 (dolist (obs (append stds dls))
1707 (ical:with-component obs
1708 ((ical:dtstart :value start)
1709 (ical:rrule :value recur-value)
1710 (ical:rdate :all rdate-nodes)
1711 (ical:tzoffsetfrom :value offset-from))
1712 ;; DTSTART of the observance must be given as local time, and is
1713 ;; combined with TZOFFSETFROM to define the effective onset
1714 ;; for the observance in absolute time.
1715 (let* ((is-daylight (ical:daylight-component-p obs))
1716 (effective-start
1717 (ical:date-time-variant start :zone offset-from
1718 :dst (not is-daylight)))
1719 (observance-might-apply
1720 (if given-clock-time
1721 (ical:date-time-locally<= effective-start given-clock-time)
1722 (ical:time<= (encode-time effective-start) given-abs-time))))
1723
1724 (when observance-might-apply
1725 ;; Initialize our return values on the first iteration
1726 ;; where an observance potentially applies:
1727 (unless nearest-onset
1728 (setq nearest-onset effective-start
1729 nearest-observance obs)
1730 (when (and update given-clock-time)
1731 (setq updated
1732 (icr:tz--get-updated-in given-clock-time
1733 effective-start obs))))
1734
1735 ;; We first check whether any RDATEs in the observance are
1736 ;; the relevant onset:
1737 (let ((rdates
1738 (mapcar #'ical:ast-node-value
1739 (apply #'append
1740 (mapcar #'ical:ast-node-value rdate-nodes)))))
1741 (dolist (rd rdates)
1742 (let* ((effective-rd
1743 ;; N.B.: we don't have to worry about rd being
1744 ;; an ical:period or ical:date here because in
1745 ;; time zone observances, RDATE values are
1746 ;; *only* allowed to be local date-times; see
1747 ;; https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5
1748 ;; and `ical:rrule-validator'
1749 (ical:date-time-variant rd :zone offset-from
1750 :dst (not is-daylight)))
1751 (onset-applies
1752 (if given-clock-time
1753 (ical:date-time-locally<= effective-rd
1754 given-clock-time)
1755 (ical:time<= (encode-time effective-rd)
1756 given-abs-time))))
1757
1758 (when (and onset-applies nearest-onset
1759 (ical:date-time< nearest-onset effective-rd))
1760 (setq nearest-onset effective-rd
1761 nearest-observance obs)
1762
1763 (when (and update given-clock-time)
1764 (setq updated
1765 (icr:tz--get-updated-in given-clock-time
1766 effective-rd obs)))))))
1767
1768 ;; If the observance has a recurrence value, it's the
1769 ;; relevant observance if it:
1770 ;; (1) has a recurrence which starts before dt
1771 ;; (2) that recurrence is the nearest in the zone
1772 ;; which starts before dt
1773 ;; Note that we intentionally do *not* pass `vtimezone'
1774 ;; through here to find-interval, recurrences-in-interval,
1775 ;; etc. so as not to cause infinite recursion. Instead we
1776 ;; directly pass `offset-from' (the offset from UTC at the
1777 ;; start of each observance onset), which
1778 ;; `icr:tz-set-zone' knows to handle specially without
1779 ;; calling this function.
1780 (when recur-value
1781 (let* ((target (or given-clock-time
1782 (decode-time given-abs-time offset-from)))
1783 (int (icr:find-interval
1784 target effective-start recur-value offset-from))
1785 (int-recs (icr:recurrences-in-interval
1786 int obs offset-from))
1787 ;; The closest observance onset before `dt' might
1788 ;; actually be in the previous interval, e.g.
1789 ;; if `dt' is in January after an annual change to
1790 ;; Standard Time in November. So check that as well.
1791 (prev-int (icr:previous-interval int recur-value
1792 effective-start
1793 offset-from))
1794 (prev-recs (when prev-int
1795 (icr:recurrences-in-interval
1796 prev-int obs offset-from)))
1797 (recs (append prev-recs int-recs))
1798 (keep-recs<=given
1799 (if given-clock-time
1800 (lambda (rec)
1801 (ical:date-time-locally<= rec given-clock-time))
1802 (lambda (rec)
1803 (ical:time<= (encode-time rec) given-abs-time))))
1804 (srecs (sort (seq-filter ; (1)
1805 keep-recs<=given
1806 recs)
1807 :lessp #'ical:date-time<
1808 :in-place t :reverse t))
1809 (latest-rec (car srecs)))
1810
1811 (when (and latest-rec
1812 (ical:date-time< nearest-onset latest-rec)) ; (2)
1813 (setf (decoded-time-dst latest-rec)
1814 ;; if obs is a DAYLIGHT observance, latest-rec
1815 ;; represents the last moment of standard time, and
1816 ;; vice versa
1817 (not is-daylight))
1818 (setq nearest-onset latest-rec
1819 nearest-observance obs)
1820 (when (and update given-clock-time)
1821 (setq updated
1822 (icr:tz--get-updated-in given-clock-time
1823 latest-rec obs))))))))))
1824
1825 ;; We've now found the nearest observance, if there was one.
1826 ;; Update `dt' as a side effect if requested. This saves
1827 ;; repeating a lot of the above in a separate function.
1828 (when (and update given-clock-time nearest-observance updated)
1829 ;; signal an error when `dt' does not exist if requested, so the
1830 ;; nonexistence can be handled further up the stack:
1831 (when (and (eq :error nonexisting)
1832 (not (ical:date-time-locally-simultaneous-p dt updated)))
1833 (signal 'ical:tz-nonexistent-time
1834 (list
1835 :message
1836 (format "%d-%02d-%02d %02d:%02d:%02d does not exist in %s"
1837 (decoded-time-year dt)
1838 (decoded-time-month dt)
1839 (decoded-time-day dt)
1840 (decoded-time-hour dt)
1841 (decoded-time-minute dt)
1842 (decoded-time-second dt)
1843 (or
1844 (ical:with-property-of nearest-observance
1845 'ical:tzname nil value)
1846 "time zone observance"))
1847 :date-time dt
1848 :observance nearest-observance)))
1849 ;; otherwise we copy `updated' over to `dt', which resets the
1850 ;; clock time in `dt' if it did not exist:
1851 (setf (decoded-time-zone dt) (decoded-time-zone updated))
1852 (setf (decoded-time-dst dt) (decoded-time-dst updated))
1853 (setf (decoded-time-second dt) (decoded-time-second updated))
1854 (setf (decoded-time-minute dt) (decoded-time-minute updated))
1855 (setf (decoded-time-hour dt) (decoded-time-hour updated))
1856 (setf (decoded-time-day dt) (decoded-time-day updated))
1857 (setf (decoded-time-month dt) (decoded-time-month updated))
1858 (setf (decoded-time-year dt) (decoded-time-year updated))
1859 (setf (decoded-time-weekday dt)
1860 (calendar-day-of-week (ical:date-time-to-date updated))))
1861
1862 ;; Return the observance and onset if found, nil if not:
1863 (when nearest-observance
1864 (list nearest-observance nearest-onset)))))
1865
1866(defun icr:tz-offset-in (observance)
1867 "Return the offset (in seconds) from UTC in effect during OBSERVANCE.
1868
1869OBSERVANCE should be an `icalendar-standard' or `icalendar-daylight'
1870subcomponent of a particular `icalendar-vtimezone'. The returned value
1871is the value of its `icalendar-tzoffsetto' property."
1872 (ical:with-property-of observance 'ical:tzoffsetto nil value))
1873
1874(defun icr:tz-decode-time (ts vtimezone)
1875 "Decode Lisp timestamp TS with the appropriate offset in VTIMEZONE.
1876
1877VTIMEZONE should be an `icalendar-vtimezone' component node. The correct
1878observance for TS will be looked up in VTIMEZONE, TS will be decoded
1879with the UTC offset of that observance, and its dst slot will be set
1880based on whether the observance is an `icalendar-standard' or
1881`icalendar-daylight' component. If VTIMEZONE does not have an
1882observance that applies to TS, it is decoded into UTC time.
1883
1884VTIMEZONE may also be an `icalendar-utc-offset'. In this case TS is
1885decoded directly into this UTC offset, and its dst slot is set to -1."
1886 (let* ((observance (when (ical:vtimezone-component-p vtimezone)
1887 (car (icr:tz-observance-on ts vtimezone))))
1888 (offset (cond (observance (icr:tz-offset-in observance))
1889 ((cl-typep vtimezone 'ical:utc-offset)
1890 vtimezone)
1891 (t 0))))
1892
1893 (ical:date-time-variant ; ensures weekday gets set, too
1894 (decode-time ts offset)
1895 :zone offset
1896 :dst (if observance (ical:daylight-component-p observance)
1897 -1))))
1898
1899(defun icr:tz-set-zone (dt vtimezone &optional nonexisting)
1900 "Set the time zone offset and dst flag in DT based on VTIMEZONE.
1901
1902DT should be an `icalendar-date-time' and VTIMEZONE should be an
1903`icalendar-vtimezone'. VTIMEZONE can also be an `icalendar-utc-offset',
1904in which case this value is directly set in DT's zone field (without
1905changing its dst flag). The updated DT is returned.
1906
1907This function generally sets only the zone and dst slots of DT, without
1908changing the other slots; its main purpose is to adjust date-times
1909generated from other date-times during recurrence rule calculations,
1910where a different time zone observance may be in effect in the original
1911date-time. It cannot be used to re-decode a fixed point in time into a
1912different time zone; for that, see `icalendar-recur-tz-decode-time'.
1913
1914If given, NONEXISTING is a keyword that specifies what to do if DT
1915represents a clock time that does not exist according to the relevant
1916observance in VTIMEZONE. The value :error means to signal an
1917\\='icalendar-tz-nonexistent-time error, and nil means to reset the
1918clock time in DT to an existing one; see
1919`icalendar-recur-tz-observance-on'."
1920 (if (cl-typep vtimezone 'ical:utc-offset)
1921 ;; This is where the recurrence rule/time zone mutual dependence
1922 ;; bottoms out; don't remove this conditional!
1923 (setf (decoded-time-zone dt) vtimezone)
1924
1925 ;; Otherwise, if there's already zone information in dt, trust it
1926 ;; without looking up the observance. This is partly a performance
1927 ;; optimization (because the lookup is expensive) and partly about
1928 ;; avoiding problems: looking up the observance uses the clock time
1929 ;; in dt without considering the zone information, and doing this
1930 ;; when dt has already been adjusted to contain valid zone
1931 ;; information can invalidate that information.
1932 ;;
1933 ;; It's reliable to skip the lookup when dt already contains zone
1934 ;; information only because `icalendar-make-date-time',
1935 ;; `icalendar-date/time-add', and in particular
1936 ;; `icalendar-date-time-variant' are careful to remove the UTC
1937 ;; offset and DST information in the date-times they construct,
1938 ;; unless provided with enough information to fill those slots.
1939 (unless (and (cl-typep dt 'ical:date-time)
1940 (decoded-time-zone dt)
1941 (booleanp (decoded-time-dst dt)))
1942 ;; This updates the relevant slots in dt as a side effect:
1943 ;; TODO: if no observance is found, is it ever sensible to signal an error,
1944 ;; instead of just leaving the zone slot unset?
1945 (icr:tz-observance-on dt vtimezone t nonexisting)))
1946 dt)
1947
1948(defun icr:tz-set-zones-in (vtimezones node)
1949 "Recursively set time zone offset and dst flags in times in NODE.
1950
1951VTIMEZONES should be a list of the `icalendar-vtimezone' components in
1952the calendar containing NODE. NODE can be any iCalendar syntax node. If
1953NODE is a property node with an `icalendar-tzidparam' parameter and an
1954`icalendar-date-time' or `icalendar-period' value, the appropriate time
1955zone observance for its value is looked up in VTIMEZONES, and used to
1956set the zone and dst slots in its value. Otherwise, the function is
1957called recursively on NODE's children."
1958 (cond
1959 ((ical:property-node-p node)
1960 (ical:with-property node
1961 ((ical:tzidparam :value tzid))
1962 (when (and tzid (eq value-type 'ical:date-time))
1963 (let* ((tz (icr:tz-for tzid vtimezones))
1964 updated)
1965 (cond ((eq value-type 'ical:date-time)
1966 (setq updated (icr:tz-set-zone value tz)))
1967 ((eq value-type 'ical:period)
1968 (setq updated
1969 (ical:make-period
1970 (icr:tz-set-zone (ical:period-start value) tz)
1971 :end
1972 (if (ical:period--defined-end value)
1973 (icr:tz-set-zone (ical:period--defined-end value) tz)
1974 (ical:period-end value tz))
1975 :duration (ical:period-dur-value value)))))
1976 (ical:ast-node-set-value value-node updated)))))
1977 ((ical:component-node-p node) ; includes VCALENDAR nodes
1978 (mapc (apply-partially #'icr:tz-set-zones-in vtimezones)
1979 (ical:ast-node-children node)))
1980 (t nil)))
1981
1982(defun icr:tzname-on (dt vtimezone)
1983 "Return the name of the time zone observance in effect on DT in VTIMEZONE.
1984
1985DT should be an `icalendar-date' or `icalendar-date-time'. VTIMEZONE
1986should be the `icalendar-vtimezone' component in which to interpret DT.
1987
1988The observance in effect on DT within VTIMEZONE is computed. The
1989returned value is the value of the `icalendar-tzname' property of this
1990observance."
1991 (when-let* ((obs/onset (icr:tz-observance-on dt vtimezone))
1992 (observance (car obs/onset)))
1993 (ical:with-property-of observance 'ical:tzname)))
1994
1995(defconst icr:-tz-warning
1996 "This time zone information was inferred from incomplete system information; it should be correct for the date-times within this calendar file referencing this zone, but you should not rely on it more widely.")
1997
1998(defconst icr:-emacs-local-tzid
1999 "Emacs_Local_")
2000
2001(defun icr:-tz-info-sexp-p (_ sexp)
2002 "Validate that SEXP gives time zone info like from `calendar-current-time-zone'."
2003 (and (listp sexp)
2004 (length= sexp 8)
2005 (let ((utc-diff (nth 0 sexp))
2006 (dst-offset (nth 1 sexp))
2007 (std-zone (nth 2 sexp))
2008 (dst-zone (nth 3 sexp))
2009 (dst-starts (nth 4 sexp))
2010 (dst-ends (nth 5 sexp))
2011 (dst-starts-time (nth 6 sexp))
2012 (dst-ends-time (nth 7 sexp)))
2013 (and
2014 (integerp utc-diff) (< (abs utc-diff) (* 60 24))
2015 (integerp dst-offset) (< (abs utc-diff) (* 60 24))
2016 (stringp std-zone)
2017 (stringp dst-zone)
2018 (or (and (listp dst-starts) (memq 'year (flatten-list dst-starts)))
2019 (and (null dst-starts) (equal std-zone dst-zone)))
2020 (or (and (listp dst-ends) (memq 'year (flatten-list dst-ends)))
2021 (and (null dst-ends) (equal std-zone dst-zone)))
2022 (or (and (integerp dst-starts-time) (< (abs dst-starts-time) (* 60 24)))
2023 (null dst-starts-time))
2024 (or (and (integerp dst-ends-time) (< (abs dst-ends-time) (* 60 24)))
2025 (null dst-ends-time))))))
2026
2027(defun icr:current-tz-to-vtimezone (&optional tz tzid start-year)
2028 "Convert TZ to an `icalendar-vtimezone'.
2029
2030TZ defaults to the output of `calendar-current-time-zone'; if specified,
2031it should be a list of the same form as that function returns.
2032Depending on TZ, this function might signal the following errors:
2033
2034`icalendar-tz-data-insufficient' if the data in TZ is not complete
2035 enough to determine time zone rules.
2036`icalendar-tz-unsupported' if the data in TZ cannot be expressed as an
2037 RFC5545 `icalendar-rrule' property.
2038
2039TZID, if specified, should be a string to identify this time zone; it
2040defaults to `icalendar-recur--emacs-local-tzid' plus the name of the
2041standard observance according to `calendar-current-time-zone'.
2042
2043START-YEAR, if specified, should be an integer giving the year in which
2044to start the observances in the time zone. It defaults to 1970."
2045 (when (and tz (not (icr:-tz-info-sexp-p nil tz)))
2046 (signal 'ical:tz-data-insufficient
2047 (list :tz tz
2048 :level 2
2049 :message
2050 "Badly formed TZ data; see `calendar-current-time-zone'")))
2051 (let* ((tzdata (or tz (calendar-current-time-zone)))
2052 (std-offset (* 60 (nth 0 tzdata)))
2053 (dst-offset (+ std-offset
2054 (* 60 (nth 1 tzdata))))
2055 (std-name (nth 2 tzdata))
2056 (dst-name (nth 3 tzdata))
2057 (dst-starts (nth 4 tzdata))
2058 (dst-ends (nth 5 tzdata))
2059 (dst-start-minutes (nth 6 tzdata))
2060 (dst-end-minutes (nth 7 tzdata)))
2061
2062 (unless (and std-offset
2063 (or (equal std-name dst-name)
2064 (and dst-starts dst-ends dst-start-minutes dst-end-minutes)))
2065 (signal 'ical:tz-data-insufficient
2066 (list :tz tz :level 2
2067 :message "Unable to create VTIMEZONE from TZ")))
2068
2069 (if (equal std-name dst-name)
2070 ;; Local time zone doesn't use DST:
2071 (ical:make-vtimezone
2072 (ical:tzid (or tzid (concat icr:-emacs-local-tzid std-name)))
2073 (ical:make-standard
2074 (ical:tzname std-name)
2075 (ical:dtstart (ical:make-date-time :year (or start-year 1970)
2076 :month 1 :day 1
2077 :hour 0 :minute 0 :second 0))
2078 (ical:tzoffsetfrom std-offset)
2079 (ical:tzoffsetto std-offset)
2080 (ical:comment icr:-tz-warning)))
2081
2082 ;; Otherwise we can provide both STANDARD and DAYLIGHT subcomponents:
2083 (let* ((std->dst-rule
2084 (if (eq (car dst-starts) 'calendar-nth-named-day)
2085 `((FREQ YEARLY)
2086 (BYMONTH (,(nth 3 dst-starts)))
2087 (BYDAY (,(cons (nth 2 dst-starts)
2088 (nth 1 dst-starts)))))
2089 ;; The only other rules that `calendar-current-time-zone'
2090 ;; can return are based on the Persian calendar, which we
2091 ;; cannot express in an `icalendar-recur' value, at least
2092 ;; pending an implementation of RFC 7529
2093 (signal 'ical:tz-unsupported
2094 (list :tz tz
2095 :level 2
2096 :message
2097 (format "Unable to export DST rule for time zone: %s"
2098 dst-starts)))))
2099 (dst-start-date (calendar-dlet ((year (or start-year 1970)))
2100 (eval dst-starts t)))
2101 (dst-start
2102 (ical:date-to-date-time dst-start-date
2103 :hour (/ dst-start-minutes 60)
2104 :minute (mod dst-start-minutes 60)
2105 :second 0))
2106 (dst->std-rule
2107 (if (eq (car dst-ends) 'calendar-nth-named-day)
2108 `((FREQ YEARLY)
2109 (BYMONTH (,(nth 3 dst-ends)))
2110 (BYDAY (,(cons (nth 2 dst-ends)
2111 (nth 1 dst-ends)))))
2112 (signal 'ical:tz-unsupported
2113 (list :tz tz
2114 :level 2
2115 :message
2116 (format "Unable to export DST rule for time zone: %s"
2117 dst-ends)))))
2118 (std-start-date (calendar-dlet ((year (1- (or start-year 1970))))
2119 (eval dst-ends t)))
2120 (std-start
2121 (ical:date-to-date-time std-start-date
2122 :hour (/ dst-end-minutes 60)
2123 :minute (mod dst-end-minutes 60)
2124 :second 0)))
2125
2126 (ical:make-vtimezone
2127 (ical:tzid (or tzid (concat icr:-emacs-local-tzid std-name)))
2128 (ical:make-standard
2129 (ical:tzname std-name)
2130 (ical:dtstart std-start)
2131 (ical:rrule dst->std-rule)
2132 (ical:tzoffsetfrom dst-offset)
2133 (ical:tzoffsetto std-offset)
2134 (ical:comment icr:-tz-warning))
2135 (ical:make-daylight
2136 (ical:tzname dst-name)
2137 (ical:dtstart dst-start)
2138 (ical:rrule std->dst-rule)
2139 (ical:tzoffsetfrom std-offset)
2140 (ical:tzoffsetto dst-offset)
2141 (ical:comment icr:-tz-warning)))))))
2142
2143(provide 'icalendar-recur)
2144
2145;; Local Variables:
2146;; read-symbol-shorthands: (("ical:" . "icalendar-") ("icr:" . "icalendar-recur-"))
2147;; End:
2148;;; icalendar-recur.el ends here
diff --git a/lisp/calendar/icalendar-utils.el b/lisp/calendar/icalendar-utils.el
new file mode 100644
index 00000000000..3f8e9d085c2
--- /dev/null
+++ b/lisp/calendar/icalendar-utils.el
@@ -0,0 +1,754 @@
1;;; icalendar-utils.el --- iCalendar utility functions -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Richard Lawrence
4
5;; Author: Richard Lawrence <rwl@recursewithless.net>
6;; Created: January 2025
7;; Keywords: calendar
8
9;; This file is part of GNU Emacs.
10
11;; This file is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15
16;; This file is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with this file. If not, see <https://www.gnu.org/licenses/>.
23
24;;; Commentary:
25
26;; This file contains a variety of utility functions to work with
27;; iCalendar data which are used throughout the rest of the iCalendar
28;; library. Most of the functions here deal with calendar and clock
29;; arithmetic, and help smooth over the type distinction between plain
30;; dates and date-times.
31
32;;; Code:
33(require 'cl-lib)
34(require 'calendar)
35(eval-when-compile (require 'icalendar-macs))
36(require 'icalendar-parser)
37
38;; Accessors for commonly used properties
39
40(defun ical:component-dtstart (component)
41 "Return the value of the `icalendar-dtstart' property of COMPONENT.
42COMPONENT can be any component node."
43 (ical:with-property-of component 'ical:dtstart nil value))
44
45(defun ical:component-dtend (component)
46 "Return the value of the `icalendar-dtend' property of COMPONENT.
47COMPONENT can be any component node."
48 (ical:with-property-of component 'ical:dtend nil value))
49
50(defun ical:component-rdate (component)
51 "Return the value of the `icalendar-rdate' property of COMPONENT.
52COMPONENT can be any component node."
53 (ical:with-property-of component 'ical:rdate nil value))
54
55(defun ical:component-summary (component)
56 "Return the value of the `icalendar-summary' property of COMPONENT.
57COMPONENT can be any component node."
58 (ical:with-property-of component 'ical:summary nil value))
59
60(defun ical:component-description (component)
61 "Return the value of the `icalendar-description' property of COMPONENT.
62COMPONENT can be any component node."
63 (ical:with-property-of component 'ical:description nil value))
64
65(defun ical:component-tzname (component)
66 "Return the value of the `icalendar-tzname' property of COMPONENT.
67COMPONENT can be any component node."
68 (ical:with-property-of component 'ical:tzname nil value))
69
70(defun ical:component-uid (component)
71 "Return the value of the `icalendar-uid' property of COMPONENT.
72COMPONENT can be any component node."
73 (ical:with-property-of component 'ical:uid nil value))
74
75(defun ical:component-url (component)
76 "Return the value of the `icalendar-url' property of COMPONENT.
77COMPONENT can be any component node."
78 (ical:with-property-of component 'ical:url nil value))
79
80(defun ical:property-tzid (property)
81 "Return the value of the `icalendar-tzid' parameter of PROPERTY."
82 (ical:with-param-of property 'ical:tzidparam nil value))
83
84;; String manipulation
85(defun ical:trimp (s &optional trim-left trim-right)
86 "Like `string-trim', but return nil if the trimmed string is empty."
87 (when (and s (stringp s))
88 (let ((trimmed (string-trim s trim-left trim-right)))
89 (unless (equal "" trimmed) trimmed))))
90
91(defun ical:strip-mailto (s)
92 "Remove \"mailto:\" case-insensitively from the start of S."
93 (let ((case-fold-search t))
94 (replace-regexp-in-string "^mailto:" "" s)))
95
96
97;; Date/time
98
99;; N.B. Notation: "date/time" is used in function names when a function
100;; can accept either `icalendar-date' or `icalendar-date-time' values;
101;; in contrast, "date-time" means it accepts *only*
102;; `icalendar-date-time' values, not plain dates.
103;; TODO: turn all the 'date/time' functions into methods dispatched by
104;; type?
105
106(defun ical:date-time-to-date (dt)
107 "Convert an `icalendar-date-time' value DT to an `icalendar-date'."
108 (list (decoded-time-month dt)
109 (decoded-time-day dt)
110 (decoded-time-year dt)))
111
112(cl-defun ical:date-to-date-time (dt &key (hour 0) (minute 0) (second 0) (tz nil))
113 "Convert an `icalendar-date' value DT to an `icalendar-date-time'.
114
115The following keyword arguments are accepted:
116 :hour, :minute, :second - integers representing a local clock time on date DT
117 :tz - an `icalendar-vtimezone' in which to interpret this clock time
118
119If these arguments are all unspecified, the hour, minute, and second
120slots of the returned date-time will be zero, and it will contain no
121time zone information. See `icalendar-make-date-time' for more on these
122arguments."
123 (ical:make-date-time
124 :year (calendar-extract-year dt)
125 :month (calendar-extract-month dt)
126 :day (calendar-extract-day dt)
127 :hour hour
128 :minute minute
129 :second second
130 :tz tz))
131
132(defun ical:date/time-to-date (dt)
133 "Extract a Gregorian date from DT.
134An `icalendar-date' value is returned unchanged.
135An `icalendar-date-time' value is converted to an `icalendar-date'."
136 (if (cl-typep dt 'ical:date)
137 dt
138 (ical:date-time-to-date dt)))
139
140;; Type-aware accessors for date/time slots that work for both ical:date
141;; and ical:date-time:
142;; NOTE: cl-typecase ONLY works here if dt is valid according to
143;; `ical:-decoded-date-time-p'! May need to adjust this if it's
144;; necessary to work with incomplete decoded-times
145(defun ical:date/time-year (dt)
146 "Return DT's year slot.
147DT may be either an `icalendar-date' or an `icalendar-date-time'."
148 (cl-typecase dt
149 (ical:date (calendar-extract-year dt))
150 (ical:date-time (decoded-time-year dt))))
151
152(defun ical:date/time-month (dt)
153 "Return DT's month slot.
154DT may be either an `icalendar-date' or an `icalendar-date-time'."
155 (cl-typecase dt
156 (ical:date (calendar-extract-month dt))
157 (ical:date-time (decoded-time-month dt))))
158
159(defun ical:date/time-monthday (dt)
160 "Return DT's day of the month slot.
161DT may be either an `icalendar-date' or an `icalendar-date-time'."
162 (cl-typecase dt
163 (ical:date (calendar-extract-day dt))
164 (ical:date-time (decoded-time-day dt))))
165
166(defun ical:date/time-weekno (dt &optional weekstart)
167 "Return DT's ISO week number.
168DT may be either an `icalendar-date' or an `icalendar-date-time'.
169WEEKSTART defaults to 1; it represents the day which starts the week,
170and should be an integer between 0 (= Sunday) and 6 (= Saturday)."
171 ;; TODO: Add support for weekstart.
172 ;; calendar-iso-from-absolute doesn't support this yet.
173 (when (and weekstart (not (= weekstart 1)))
174 (error "Support for WEEKSTART other than 1 (=Monday) not implemented yet"))
175 (let* ((gdate (ical:date/time-to-date dt))
176 (isodate (calendar-iso-from-absolute
177 (calendar-absolute-from-gregorian gdate)))
178 (weekno (car isodate)))
179 weekno))
180
181(defun ical:date/time-weekday (dt)
182 "Return DT's day of the week.
183DT may be either an `icalendar-date' or an `icalendar-date-time'."
184 (cl-typecase dt
185 (ical:date (calendar-day-of-week dt))
186 (ical:date-time
187 (or (decoded-time-weekday dt)
188 ;; compensate for possibly-nil weekday slot if the date-time
189 ;; has been constructed by `make-decoded-time'; cf. comment
190 ;; in `icalendar--decoded-date-time-p':
191 (calendar-day-of-week (ical:date-time-to-date dt))))))
192
193(defun ical:date/time-hour (dt)
194 "Return DT's hour slot, or nil.
195DT may be either an `icalendar-date' or an `icalendar-date-time'."
196 (when (cl-typep dt 'ical:date-time)
197 (decoded-time-hour dt)))
198
199(defun ical:date/time-minute (dt)
200 "Return DT's minute slot, or nil.
201DT may be either an `icalendar-date' or an `icalendar-date-time'."
202 (when (cl-typep dt 'ical:date-time)
203 (decoded-time-minute dt)))
204
205(defun ical:date/time-second (dt)
206 "Return DT's second slot, or nil.
207DT may be either an `icalendar-date' or an `icalendar-date-time'."
208 (when (cl-typep dt 'ical:date-time)
209 (decoded-time-second dt)))
210
211(defun ical:date/time-zone (dt)
212 "Return DT's time zone slot, or nil.
213DT may be either an `icalendar-date' or an `icalendar-date-time'."
214 (when (cl-typep dt 'ical:date-time)
215 (decoded-time-zone dt)))
216
217;;; Date/time comparisons and arithmetic:
218(defun ical:date< (dt1 dt2)
219 "Return non-nil if date DT1 is strictly earlier than date DT2.
220DT1 and DT2 must both be `icalendar-date' values of the form (MONTH DAY YEAR)."
221 (< (calendar-absolute-from-gregorian dt1)
222 (calendar-absolute-from-gregorian dt2)))
223
224(defun ical:date<= (dt1 dt2)
225 "Return non-nil if date DT1 is earlier than or the same date as DT2.
226DT1 and DT2 must both be `icalendar-date' values of the form (MONTH DAY YEAR)."
227 (or (calendar-date-equal dt1 dt2) (ical:date< dt1 dt2)))
228
229(defun ical:date-time-locally-earlier (dt1 dt2 &optional or-equal)
230 "Return non-nil if date-time DT1 is locally earlier than DT2.
231
232Unlike `icalendar-date-time<', this function assumes both times are
233local to some time zone and does not consider their zone information.
234
235If OR-EQUAL is non-nil, this function acts like `<=' rather than `<':
236it will return non-nil if DT1 and DT2 are locally the same time."
237 (let ((year1 (decoded-time-year dt1))
238 (year2 (decoded-time-year dt2))
239 (month1 (decoded-time-month dt1))
240 (month2 (decoded-time-month dt2))
241 (day1 (decoded-time-day dt1))
242 (day2 (decoded-time-day dt2))
243 (hour1 (decoded-time-hour dt1))
244 (hour2 (decoded-time-hour dt2))
245 (minute1 (decoded-time-minute dt1))
246 (minute2 (decoded-time-minute dt2))
247 (second1 (decoded-time-second dt1))
248 (second2 (decoded-time-second dt2)))
249 (or (< year1 year2)
250 (and (= year1 year2)
251 (or (< month1 month2)
252 (and (= month1 month2)
253 (or (< day1 day2)
254 (and (= day1 day2)
255 (or (< hour1 hour2)
256 (and (= hour1 hour2)
257 (or (< minute1 minute2)
258 (and (= minute1 minute2)
259 (if or-equal
260 (<= second1 second2)
261 (< second1 second2))))))))))))))
262
263(defun ical:date-time-locally< (dt1 dt2)
264 "Return non-nil if date-time DT1 is locally strictly earlier than DT2.
265
266Unlike `icalendar-date-time<', this function assumes both times are
267local to some time zone and does not consider their zone information."
268 (ical:date-time-locally-earlier dt1 dt2 nil))
269
270(defun ical:date-time-locally<= (dt1 dt2)
271 "Return non-nil if date-time DT1 is locally earlier than, or equal to, DT2.
272
273Unlike `icalendar-date-time<=', this function assumes both times are
274local to some time zone and does not consider their zone information."
275 (ical:date-time-locally-earlier dt1 dt2 t))
276
277(defun ical:date-time< (dt1 dt2)
278 "Return non-nil if date-time DT1 is strictly earlier than DT2.
279
280DT1 and DT2 must both be decoded times, and either both or neither
281should have time zone information.
282
283If one has a time zone offset and the other does not, the offset
284returned from `current-time-zone' is used as the missing offset; if
285`current-time-zone' cannot provide this information, an error is
286signaled."
287 (let ((zone1 (decoded-time-zone dt1))
288 (zone2 (decoded-time-zone dt2)))
289 (cond ((and (integerp zone1) (integerp zone2))
290 (time-less-p (encode-time dt1) (encode-time dt2)))
291 ((and (null zone1) (null zone2))
292 (ical:date-time-locally< dt1 dt2))
293 (t
294 ;; Cf. RFC5545 Sec. 3.3.5:
295 ;; "The recipient of an iCalendar object with a property value
296 ;; consisting of a local time, without any relative time zone
297 ;; information, SHOULD interpret the value as being fixed to whatever
298 ;; time zone the "ATTENDEE" is in at any given moment. This means
299 ;; that two "Attendees", in different time zones, receiving the same
300 ;; event definition as a floating time, may be participating in the
301 ;; event at different actual times. Floating time SHOULD only be
302 ;; used where that is the reasonable behavior."
303 ;; I'm interpreting this to mean that if we get here, where
304 ;; one date-time has zone information and the other doesn't,
305 ;; we should use the offset from (current-time-zone).
306 (let* ((user-tz (current-time-zone))
307 (user-offset (car user-tz))
308 (dt1z (ical:date-time-variant dt1 :zone (or zone1 user-offset)))
309 (dt2z (ical:date-time-variant dt2 :zone (or zone2 user-offset))))
310 (if user-offset
311 (time-less-p (encode-time dt1z) (encode-time dt2z))
312 (error "Too little zone information for comparison: %s %s"
313 dt1 dt2)))))))
314
315;; Two different notions of equality are relevant to decoded times:
316;; strict equality (`icalendar-date-time=') of all slots, or
317;; simultaneity (`icalendar-date-time-simultaneous-p').
318;; Most tests probably want the strict notion, because it distinguishes
319;; between simultaneous events decoded into different time zones,
320;; whereas most user-facing functions (e.g. sorting events by date and time)
321;; probably want simultaneity.
322(defun ical:date-time= (dt1 dt2)
323 "Return non-nil if DT1 and DT2 are decoded-times with identical slot values.
324
325Note that this function returns nil if DT1 and DT2 represent times in
326different time zones, even if they are simultaneous. For the latter, see
327`icalendar-date-time-simultaneous-p'."
328 (equal dt1 dt2))
329
330(defun ical:date-time-locally-simultaneous-p (dt1 dt2)
331 "Return non-nil if DT1 and DT2 are locally simultaneous date-times.
332Note that this function ignores zone information in dt1 and dt2. It
333returns non-nil if DT1 and DT2 represent the same clock time in
334different time zones, even if they encode to different absolute times."
335 (and (eq (decoded-time-year dt1) (decoded-time-year dt2))
336 (eq (decoded-time-month dt1) (decoded-time-month dt2))
337 (eq (decoded-time-day dt1) (decoded-time-day dt2))
338 (eq (decoded-time-hour dt1) (decoded-time-hour dt2))
339 (eq (decoded-time-minute dt1) (decoded-time-minute dt2))
340 (eq (decoded-time-second dt1) (decoded-time-second dt2))))
341
342(defun ical:date-time-simultaneous-p (dt1 dt2)
343 "Return non-nil if DT1 and DT2 are simultaneous date-times.
344
345This function returns non-nil if DT1 and DT2 encode to the same Lisp
346timestamp. Thus they can count as simultaneous even if they represent
347times in different timezones. If both date-times lack an offset from
348UTC, they are treated as simultaneous if they encode to the same
349timestamp in UTC.
350
351If only one date-time has an offset, they are treated as
352non-simultaneous if they represent different clock times according to
353`icalendar-date-time-locally-simultaneous-p'. Otherwise an error is
354signaled."
355 (let ((zone1 (decoded-time-zone dt1))
356 (zone2 (decoded-time-zone dt2)))
357 (cond ((and (integerp zone1) (integerp zone2))
358 (time-equal-p (encode-time dt1) (encode-time dt2)))
359 ((and (null zone1) (null zone2))
360 (time-equal-p (encode-time (ical:date-time-variant dt1 :zone 0))
361 (encode-time (ical:date-time-variant dt2 :zone 0))))
362 (t
363 ;; Best effort:
364 ;; TODO: I'm not convinced this is the right thing to do yet.
365 ;; Might want to be stricter here and fix the problem of comparing
366 ;; times with and without zone information elsewhere.
367 (if (ical:date-time-locally-simultaneous-p dt1 dt2)
368 (error "Missing zone information: %s %s" dt1 dt2)
369 nil)))))
370
371(defun ical:date-time<= (dt1 dt2)
372 "Return non-nil if DT1 is earlier than, or simultaneous with, DT2.
373DT1 and DT2 must both be decoded times, and either both or neither must have
374time zone information."
375 (or (ical:date-time< dt1 dt2)
376 (ical:date-time-simultaneous-p dt1 dt2)))
377
378(defun ical:date/time< (dt1 dt2)
379 "Return non-nil if DT1 is strictly earlier than DT2.
380DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time'
381values. If they are not of the same type, only the date in the
382`icalendar-date-time' value will be considered."
383 (cl-typecase dt1
384 (ical:date
385 (if (cl-typep dt2 'ical:date)
386 (ical:date< dt1 dt2)
387 (ical:date< dt1 (ical:date-time-to-date dt2))))
388
389 (ical:date-time
390 (if (cl-typep dt2 'ical:date-time)
391 (ical:date-time< dt1 dt2)
392 (ical:date< (ical:date-time-to-date dt1) dt2)))))
393
394(defun ical:date/time<= (dt1 dt2)
395 "Return non-nil if DT1 is earlier than or simultaneous to DT2.
396DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time'
397values. If they are not of the same type, only the date in the
398`icalendar-date-time' value will be considered."
399 (cl-typecase dt1
400 (ical:date
401 (if (cl-typep dt2 'ical:date)
402 (ical:date<= dt1 dt2)
403 (ical:date<= dt1 (ical:date-time-to-date dt2))))
404
405 (ical:date-time
406 (if (cl-typep dt2 'ical:date-time)
407 (ical:date-time<= dt1 dt2)
408 (ical:date<= (ical:date-time-to-date dt1) dt2)))))
409
410(defun ical:date/time-min (&rest dts)
411 "Return the earliest date or date-time among DTS.
412
413The DTS may be any `icalendar-date' or `icalendar-date-time' values, and
414will be ordered by `icalendar-date/time<='."
415 (car (sort dts :lessp #'ical:date/time<=)))
416
417(defun ical:date/time-max (&rest dts)
418 "Return the latest date or date-time among DTS.
419
420The DTS may be any `icalendar-date' or `icalendar-date-time' values, and
421will be ordered by `icalendar-date/time<='."
422 (car (sort dts :reverse t :lessp #'ical:date/time<=)))
423
424(defun ical:date-add (date unit n)
425 "Add N UNITs to DATE.
426
427UNIT should be `:year', `:month', `:week', or `:day'; time units will be
428ignored. N may be a positive or negative integer."
429 (if (memq unit '(:hour :minute :second))
430 date
431 (let* ((dt (ical:make-date-time :year (calendar-extract-year date)
432 :month (calendar-extract-month date)
433 :day (calendar-extract-day date)))
434 (delta (if (eq unit :week)
435 (make-decoded-time :day (* 7 n))
436 (make-decoded-time unit n)))
437 (new-dt (decoded-time-add dt delta)))
438 (ical:date-time-to-date new-dt))))
439
440(defun ical:date-time-add (dt delta &optional vtimezone)
441 "Like `decoded-time-add', but also updates weekday and time zone slots.
442
443DT and DELTA should be `icalendar-date-time' values (decoded times), as
444in `decoded-time-add'. VTIMEZONE, if given, should be an
445`icalendar-vtimezone'. The resulting date-time will be given the offset
446determined by VTIMEZONE at the local time determined by adding DELTA to
447DT.
448
449This function assumes that time units in DELTA larger than an hour
450should not affect the local clock time in the result, even when crossing
451an observance boundary in VTIMEZONE. This means that e.g. if DT is at
4529AM daylight savings time on the day before the transition to standard
453time, then the result of adding a DELTA of two days will be at 9AM
454standard time, even though this is not exactly 48 hours later. Adding a
455DELTA of 48 hours, on the other hand, will result in a time exactly 48
456hours later, but at a different local time."
457 (require 'icalendar-recur) ; for icr:tz-decode-time; avoids circular requires
458 (declare-function icalendar-recur-tz-decode-time "icalendar-recur")
459
460 (if (not vtimezone)
461 ;; the simple case: we have no time zone info, so just use
462 ;; `decoded-time-add':
463 (let ((sum (decoded-time-add dt delta)))
464 (ical:date-time-variant sum))
465 ;; `decoded-time-add' does not take time zone shifts into account,
466 ;; so we need to do the adjustment ourselves. We first add the units
467 ;; larger than an hour using `decoded-time-add', holding the clock
468 ;; time fixed, as described in the docstring. Then we add the time
469 ;; units as a fixed number of seconds and re-decode the resulting
470 ;; absolute time into the time zone.
471 (let* ((cal-delta (make-decoded-time :year (or (decoded-time-year delta) 0)
472 :month (or (decoded-time-month delta) 0)
473 :day (or (decoded-time-day delta) 0)))
474 (cal-sum (decoded-time-add dt cal-delta))
475 (dt-w/zone (ical:date-time-variant cal-sum
476 :tz vtimezone))
477 (secs-delta (+ (or (decoded-time-second delta) 0)
478 (* 60 (or (decoded-time-minute delta) 0))
479 (* 60 60 (or (decoded-time-hour delta) 0))))
480 (sum-ts (time-add (encode-time dt-w/zone) secs-delta)))
481 (icalendar-recur-tz-decode-time sum-ts vtimezone))))
482
483;; TODO: rework so that it's possible to add dur-values to plain dates.
484;; Perhaps rename this to "date/time-inc" or so, or use kwargs to allow
485;; multiple units, or...
486(defun ical:date/time-add (dt unit n &optional vtimezone)
487 "Add N UNITs to DT.
488
489DT should be an `icalendar-date' or `icalendar-date-time'. UNIT should
490be `:year', `:month', `:week', `:day', `:hour', `:minute', or `:second';
491time units will be ignored if DT is an `icalendar-date'. N may be a
492positive or negative integer."
493 (cl-typecase dt
494 (ical:date-time
495 (let ((delta (if (eq unit :week) (make-decoded-time :day (* 7 n))
496 (make-decoded-time unit n))))
497 (ical:date-time-add dt delta vtimezone)))
498 (ical:date (ical:date-add dt unit n))))
499
500(defun ical:date/time-add-duration (start duration &optional vtimezone)
501 "Return the end date(-time) which is a length of DURATION after START.
502
503START should be an `icalendar-date' or `icalendar-date-time'; the
504returned value will be of the same type as START. DURATION should be an
505`icalendar-dur-value'. VTIMEZONE, if specified, should be the
506`icalendar-vtimezone' representing the time zone of START."
507 (if (integerp duration)
508 ;; number of weeks:
509 (setq duration (make-decoded-time :day (* 7 duration))))
510 (cl-typecase start
511 (ical:date
512 (ical:date-time-to-date
513 (ical:date-time-add (ical:date-to-date-time start) duration)))
514 (ical:date-time
515 (ical:date-time-add start duration vtimezone))))
516
517(defun ical:duration-between (start end)
518 "Return the duration between START and END.
519
520START should be an `icalendar-date' or `icalendar-date-time'; END must
521be of the same type as START. The returned value is an
522`icalendar-dur-value', i.e., a time delta in the sense of
523`decoded-time-add'."
524 (cl-typecase start
525 (ical:date
526 (make-decoded-time :day (- (calendar-absolute-from-gregorian end)
527 (calendar-absolute-from-gregorian start))))
528 (ical:date-time
529 (let* ((start-abs (time-convert (encode-time start) 'integer))
530 (end-abs (time-convert (encode-time end) 'integer))
531 (dur-secs (- end-abs start-abs))
532 (days (/ dur-secs (* 60 60 24)))
533 (dur-nodays (mod dur-secs (* 60 60 24)))
534 (hours (/ dur-nodays (* 60 60)))
535 (dur-nohours (mod dur-nodays (* 60 60)))
536 (minutes (/ dur-nohours 60))
537 (seconds (mod dur-nohours 60)))
538 (make-decoded-time :day days
539 :hour hours :minute minutes :second seconds)))))
540
541(defun ical:date/time-to-local (dt)
542 "Reinterpret DT in Emacs local time if necessary.
543If DT is an `icalendar-date-time', encode and re-decode it into Emacs
544local time. If DT is an `icalendar-date', return it unchanged."
545 (cl-typecase dt
546 (ical:date dt)
547 (ical:date-time
548 (ical:date-time-variant ; ensure weekday is present too
549 (decode-time (encode-time dt))))))
550
551(defun ical:dates-until (start end &optional locally)
552 "Return a list of `icalendar-date' values between START and END.
553
554START and END may be either `icalendar-date' or `icalendar-date-time'
555values. START is an inclusive lower bound, and END is an exclusive
556upper bound. (Note, however, that if END is a date-time and its time is
557after midnight, then its date will be included in the returned list.)
558
559If LOCALLY is non-nil and START and END are date-times, these will be
560interpreted into Emacs local time, so that the dates returned are valid
561for the local time zone."
562 (require 'icalendar-recur) ; avoid circular requires
563 (declare-function icalendar-recur-subintervals-to-dates "icalendar-recur")
564
565 (when locally
566 (when (cl-typep start 'ical:date-time)
567 (setq start (ical:date/time-to-local start)))
568 (when (cl-typep end 'ical:date-time)
569 (setq end (ical:date/time-to-local end))))
570 (cl-typecase start
571 (ical:date
572 (cl-typecase end
573 (ical:date
574 (icalendar-recur-subintervals-to-dates
575 (list (list (ical:date-to-date-time start)
576 (ical:date-to-date-time end)))))
577 (ical:date-time
578 (icalendar-recur-subintervals-to-dates
579 (list (list (ical:date-to-date-time start) end))))))
580 (ical:date-time
581 (cl-typecase end
582 (ical:date
583 (icalendar-recur-subintervals-to-dates
584 (list (list start (ical:date-to-date-time end)))))
585 (ical:date-time
586 (icalendar-recur-subintervals-to-dates (list (list start end))))))))
587
588
589(cl-defun ical:make-date-time (&key second minute hour day month year
590 (dst -1 given-dst) zone tz)
591 "Make an `icalendar-date-time' from the given keyword arguments.
592
593This function is like `make-decoded-time', except that it automatically
594sets the weekday slot set based on the date arguments, and it accepts an
595additional keyword argument: `:tz'. If provided, its value should be an
596`icalendar-vtimezone', and the `:zone' and `:dst' arguments should not
597be provided. In this case, the zone and dst slots in the returned
598date-time will be adjusted to the correct values in the given time zone
599for the local time represented by the remaining arguments."
600 (when (and tz (or zone given-dst))
601 (error "Possibly conflicting time zone data in args"))
602 (apply #'ical:date-time-variant (make-decoded-time)
603 `(:second ,second :minute ,minute :hour ,hour
604 :day ,day :month ,month :year ,year
605 ;; Don't pass these keywords unless they were given explicitly.
606 ;; TODO: is there a cleaner way to write this?
607 ,@(when tz (list :tz tz))
608 ,@(when given-dst (list :dst dst))
609 ,@(when zone (list :zone zone)))))
610
611(cl-defun ical:date-time-variant (dt &key second minute hour
612 day month year
613 (dst -1 given-dst)
614 (zone nil given-zone)
615 tz)
616 "Return a variant of DT with slots modified as in the given arguments.
617
618DT should be an `icalendar-date-time'; the keyword arguments have the
619same meanings as in `make-decoded-time'. The returned variant will have
620slot values as specified by the arguments or copied from DT, except that
621the weekday slot will be updated if necessary, and the zone and dst
622fields will not be set unless given explicitly (because varying the date
623and clock time generally invalidates the time zone information in DT).
624
625One additional keyword argument is accepted: `:tz'. If provided, its
626value should be an `icalendar-vtimezone', an `icalendar-utc-offset', or
627the symbol \\='preserve. If it is a time zone component, the zone and
628dst slots in the returned variant will be adjusted to the correct
629values in the given time zone for the local time represented by the
630variant. If it is a UTC offset, the variant's zone slot will contain
631this value, but its dst slot will not be adjusted. If it is the symbol
632\\='preserve, then both the zone and dst fields are copied from DT into
633the variant."
634 (require 'icalendar-recur) ; for icr:tz-set-zone; avoids circular requires
635 (declare-function icalendar-recur-tz-set-zone "icalendar-recur")
636
637 (let ((variant
638 (make-decoded-time :second (or second (decoded-time-second dt))
639 :minute (or minute (decoded-time-minute dt))
640 :hour (or hour (decoded-time-hour dt))
641 :day (or day (decoded-time-day dt))
642 :month (or month (decoded-time-month dt))
643 :year (or year (decoded-time-year dt))
644 ;; For zone and dst slots, trust the value
645 ;; if explicitly specified or explicitly
646 ;; requested to preserve, but not otherwise
647 :dst (cond (given-dst dst)
648 ((eq 'preserve tz) (decoded-time-dst dt))
649 (t -1))
650 :zone (cond (given-zone zone)
651 ((eq 'preserve tz) (decoded-time-zone dt))
652 (t nil)))))
653 ;; update weekday slot when possible, since it depends on the date
654 ;; slots, which might have changed. (It's not always possible,
655 ;; because pure time values are also represented as decoded-times,
656 ;; with empty date slots.)
657 (unless (or (null (decoded-time-year variant))
658 (null (decoded-time-month variant))
659 (null (decoded-time-day variant)))
660 (setf (decoded-time-weekday variant)
661 (calendar-day-of-week (ical:date-time-to-date variant))))
662 ;; if given a time zone or UTC offset, update zone and dst slots,
663 ;; which also might have changed:
664 (when (and tz (not (eq 'preserve tz)))
665 (icalendar-recur-tz-set-zone variant tz))
666 variant))
667
668(defun ical:date/time-in-period-p (dt period &optional vtimezone)
669 "Return non-nil if DT occurs within PERIOD.
670
671DT can be an `icalendar-date' or `icalendar-date-time' value. PERIOD
672should be an `icalendar-period' value. VTIMEZONE, if given, is passed
673to `icalendar-period-end' to compute the end time of the period if it
674was not specified explicitly."
675 (and (ical:date/time<= (ical:period-start period) dt)
676 (ical:date/time< dt (ical:period-end period vtimezone))))
677
678;; TODO: surely this exists already?
679(defun ical:time<= (a b)
680 "Compare two Lisp timestamps A and B: is A <= B?"
681 (or (time-equal-p a b)
682 (time-less-p a b)))
683
684(defun ical:number-of-weeks (year &optional weekstart)
685 "Return the number of weeks in (Gregorian) YEAR.
686
687RFC5545 defines week 1 as the first week to include at least four days
688in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART
689is specified, in which case it should be an integer between 0 (= Sunday)
690and 6 (= Saturday)."
691 ;; There are 53 weeks in a year if Jan 1 is the fourth day after
692 ;; WEEKSTART, e.g. if the week starts on Monday and Jan 1 is a
693 ;; Thursday, or in a leap year if Jan 1 is the third day after WEEKSTART
694 (let* ((jan1wd (calendar-day-of-week (list 1 1 year)))
695 (delta (mod (- jan1wd (or weekstart 1)) 7)))
696 (if (or (= 4 delta)
697 (and (= 3 delta) (calendar-leap-year-p year)))
698 53
699 52)))
700
701(defun ical:start-of-weekno (weekno year &optional weekstart)
702 "Return the start of the WEEKNOth week in the (Gregorian) YEAR.
703
704RFC5545 defines week 1 as the first week to include at least four days
705in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART
706is specified, in which case it should be an integer between 0 (= Sunday)
707and 6 (= Saturday). The returned value is an `icalendar-date'.
708
709If WEEKNO is negative, it refers to the WEEKNOth week before the end of
710the year: -1 is the last week of the year, -2 second to last, etc."
711 (calendar-gregorian-from-absolute
712 (+
713 (* 7 (if (< 0 weekno)
714 (1- weekno)
715 (+ 1 weekno (ical:number-of-weeks year weekstart))))
716 (calendar-dayname-on-or-before
717 (or weekstart 1)
718 ;; Three days after Jan 1. gives us the nearest occurrence;
719 ;; see `calendar-dayname-on-or-before'
720 (+ 3 (calendar-absolute-from-gregorian (list 1 1 year)))))))
721
722(defun ical:nth-weekday-in (n weekday year &optional month)
723 "Return the Nth WEEKDAY in YEAR or MONTH.
724
725If MONTH is specified, it refers to MONTH in YEAR, and N acts as an
726index for WEEKDAYs within the month. Otherwise, N acts as an index for
727WEEKDAYs within the entire YEAR.
728
729N should be an integer. If N<0, it counts from the end of the month or
730year: if N=-1, it refers to the last WEEKDAY in the month or year, if
731N=-2 the second to last, and so on."
732 (if month
733 (calendar-nth-named-day n weekday month year)
734 (let* ((jan1 (calendar-absolute-from-gregorian (list 1 1 year)))
735 (dec31 (calendar-absolute-from-gregorian (list 12 31 year))))
736 ;; Adapted from `calendar-nth-named-absday'.
737 ;; TODO: we could generalize that function to make month an optional
738 ;; argument, but that would mean changing its interface.
739 (calendar-gregorian-from-absolute
740 (if (> n 0)
741 (+ (* 7 (1- n))
742 (calendar-dayname-on-or-before
743 weekday
744 (+ 6 jan1)))
745 (+ (* 7 (1+ n))
746 (calendar-dayname-on-or-before
747 weekday
748 dec31)))))))
749
750(provide 'icalendar-utils)
751;; Local Variables:
752;; read-symbol-shorthands: (("ical:" . "icalendar-"))
753;; End:
754;;; icalendar-utils.el ends here
diff --git a/lisp/calendar/icalendar.el b/lisp/calendar/icalendar.el
index 84a83284537..4956dc82f09 100644
--- a/lisp/calendar/icalendar.el
+++ b/lisp/calendar/icalendar.el
@@ -26,6 +26,11 @@
26 26
27;;; Commentary: 27;;; Commentary:
28 28
29;; Most of the code in this file is now obsolete and has been marked as such.
30;; For the new implementation of diary import/export, see diary-icalendar.el.
31;; Error handling code, global variables, and user options relevant for the
32;; entire iCalendar library remain in this file.
33
29;; This package is documented in the Emacs Manual. 34;; This package is documented in the Emacs Manual.
30 35
31;; Please note: 36;; Please note:
@@ -73,39 +78,11 @@
73;; 0.01: (2003-03-21) 78;; 0.01: (2003-03-21)
74;; - First published version. Trial version. Alpha version. 79;; - First published version. Trial version. Alpha version.
75 80
76;; ======================================================================
77;; To Do:
78
79;; * Import from ical to diary:
80;; + Need more properties for icalendar-import-format
81;; (added all that Mozilla Calendar uses)
82;; From iCal specifications (RFC2445: 4.8.1), icalendar.el lacks
83;; ATTACH, CATEGORIES, COMMENT, GEO, PERCENT-COMPLETE (VTODO),
84;; PRIORITY, RESOURCES) not considering date/time and time-zone
85;; + check vcalendar version
86;; + check (unknown) elements
87;; + recurring events!
88;; + works for european style calendars only! Does it?
89;; + alarm
90;; + exceptions in recurring events
91;; + the parser is too soft
92;; + error log is incomplete
93;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
94;; + timezones probably still need some improvements.
95
96;; * Export from diary to ical
97;; + diary-date, diary-float, and self-made sexp entries are not
98;; understood
99
100;; * Other things
101;; + clean up all those date/time parsing functions
102;; + Handle todo items?
103;; + Check iso 8601 for datetime and period
104;; + Which chars to (un)escape?
105
106
107;;; Code: 81;;; Code:
108 82
83(eval-when-compile (require 'compile))
84(eval-when-compile (require 'cl-lib))
85
109;; ====================================================================== 86;; ======================================================================
110;; Customizables 87;; Customizables
111;; ====================================================================== 88;; ======================================================================
@@ -135,48 +112,78 @@ argument. It must return a string. See
135`icalendar-import-format-sample' for an example." 112`icalendar-import-format-sample' for an example."
136 :type '(choice 113 :type '(choice
137 (string :tag "String") 114 (string :tag "String")
138 (function :tag "Function")) 115 (function :tag "Function")))
139 :group 'icalendar) 116
117(make-obsolete-variable
118 'icalendar-import-format
119 "please use `diary-icalendar-vevent-format-function' for import
120formatting instead."
121 "31.1")
140 122
141(defcustom icalendar-import-format-summary 123(defcustom icalendar-import-format-summary
142 "%s" 124 "%s"
143 "Format string defining how the summary element is formatted. 125 "Format string defining how the summary element is formatted.
144This applies only if the summary is not empty! `%s' is replaced 126This applies only if the summary is not empty! `%s' is replaced
145by the summary." 127by the summary."
146 :type 'string 128 :type 'string)
147 :group 'icalendar) 129
130(make-obsolete-variable
131 'icalendar-import-format-summary
132 "please use `diary-icalendar-vevent-format-function' for import
133formatting instead."
134 "31.1")
148 135
149(defcustom icalendar-import-format-description 136(defcustom icalendar-import-format-description
150 "\n Desc: %s" 137 "\n Desc: %s"
151 "Format string defining how the description element is formatted. 138 "Format string defining how the description element is formatted.
152This applies only if the description is not empty! `%s' is 139This applies only if the description is not empty! `%s' is
153replaced by the description." 140replaced by the description."
154 :type 'string 141 :type 'string)
155 :group 'icalendar) 142
143(make-obsolete-variable
144 'icalendar-import-format-description
145 "please use `diary-icalendar-vevent-format-function' for import
146formatting instead."
147 "31.1")
156 148
157(defcustom icalendar-import-format-location 149(defcustom icalendar-import-format-location
158 "\n Location: %s" 150 "\n Location: %s"
159 "Format string defining how the location element is formatted. 151 "Format string defining how the location element is formatted.
160This applies only if the location is not empty! `%s' is replaced 152This applies only if the location is not empty! `%s' is replaced
161by the location." 153by the location."
162 :type 'string 154 :type 'string)
163 :group 'icalendar) 155
156(make-obsolete-variable
157 'icalendar-import-format-location
158 "please use `diary-icalendar-vevent-format-function' for import
159formatting instead."
160 "31.1")
164 161
165(defcustom icalendar-import-format-organizer 162(defcustom icalendar-import-format-organizer
166 "\n Organizer: %s" 163 "\n Organizer: %s"
167 "Format string defining how the organizer element is formatted. 164 "Format string defining how the organizer element is formatted.
168This applies only if the organizer is not empty! `%s' is 165This applies only if the organizer is not empty! `%s' is
169replaced by the organizer." 166replaced by the organizer."
170 :type 'string 167 :type 'string)
171 :group 'icalendar) 168
169(make-obsolete-variable
170 'icalendar-import-format-organizer
171 "please use `diary-icalendar-vevent-format-function' for import
172formatting instead."
173 "31.1")
172 174
173(defcustom icalendar-import-format-url 175(defcustom icalendar-import-format-url
174 "\n URL: %s" 176 "\n URL: %s"
175 "Format string defining how the URL element is formatted. 177 "Format string defining how the URL element is formatted.
176This applies only if the URL is not empty! `%s' is replaced by 178This applies only if the URL is not empty! `%s' is replaced by
177the URL." 179the URL."
178 :type 'string 180 :type 'string)
179 :group 'icalendar) 181
182(make-obsolete-variable
183 'icalendar-import-format-url
184 "please use `diary-icalendar-vevent-format-function' for import
185formatting instead."
186 "31.1")
180 187
181(defcustom icalendar-import-format-uid 188(defcustom icalendar-import-format-uid
182 "\n UID: %s" 189 "\n UID: %s"
@@ -184,87 +191,91 @@ the URL."
184This applies only if the UID is not empty! `%s' is replaced by 191This applies only if the UID is not empty! `%s' is replaced by
185the UID." 192the UID."
186 :type 'string 193 :type 'string
187 :version "24.3" 194 :version "24.3")
188 :group 'icalendar) 195
196(make-obsolete-variable
197 'icalendar-import-format-uid
198 "please use `diary-icalendar-vevent-format-function' for import
199formatting instead."
200 "31.1")
189 201
190(defcustom icalendar-import-format-status 202(defcustom icalendar-import-format-status
191 "\n Status: %s" 203 "\n Status: %s"
192 "Format string defining how the status element is formatted. 204 "Format string defining how the status element is formatted.
193This applies only if the status is not empty! `%s' is replaced by 205This applies only if the status is not empty! `%s' is replaced by
194the status." 206the status."
195 :type 'string 207 :type 'string)
196 :group 'icalendar) 208
209(make-obsolete-variable
210 'icalendar-import-format-status
211 "please use `diary-icalendar-vevent-format-function' for import
212formatting instead."
213 "31.1")
197 214
198(defcustom icalendar-import-format-class 215(defcustom icalendar-import-format-class
199 "\n Class: %s" 216 "\n Class: %s"
200 "Format string defining how the class element is formatted. 217 "Format string defining how the class element is formatted.
201This applies only if the class is not empty! `%s' is replaced by 218This applies only if the class is not empty! `%s' is replaced by
202the class." 219the class."
203 :type 'string 220 :type 'string)
204 :group 'icalendar) 221
205 222(make-obsolete-variable
206(defcustom icalendar-recurring-start-year 223 'icalendar-import-format-class
207 2005 224 "please use `diary-icalendar-vevent-format-function' for import
208 "Start year for recurring events. 225formatting instead."
209Some calendar browsers only propagate recurring events for 226 "31.1")
210several years beyond the start time. Set this string to a year 227
211just before the start of your personal calendar." 228(define-obsolete-variable-alias
212 :type 'integer 229 'icalendar-recurring-start-year
213 :group 'icalendar) 230 'diary-icalendar-recurring-start-year
214 231 "31.1")
215(defcustom icalendar-export-hidden-diary-entries 232
216 t 233(define-obsolete-variable-alias
217 "Determines whether hidden diary entries are exported. 234 'icalendar-export-hidden-diary-entries
218If non-nil hidden diary entries (starting with `&') get exported, 235 'diary-icalendar-export-nonmarking-entries
219if nil they are ignored." 236 "31.1")
220 :type 'boolean 237
221 :group 'icalendar) 238(defcustom ical:uid-format
222 239 "%h"
223(defcustom icalendar-uid-format 240 "Format string for unique ID (UID) values for iCalendar components.
224 "emacs%t%c" 241
225 "Format of unique ID code (UID) for each iCalendar object. 242This string is used by `icalendar-make-uid' to generate UID values when
243creating iCalendar components.
244
226The following specifiers are available: 245The following specifiers are available:
227%c COUNTER, an integer value that is increased each time a uid is 246%c COUNTER, an integer value that is increased each time a uid is
228 generated. This may be necessary for systems which do not 247 generated. This may be necessary for systems which do not
229 provide time-resolution finer than a second. 248 provide time-resolution finer than a second.
230%h HASH, a hash value of the diary entry, 249%h HASH, a hash value of the component's contents or system information,
231%s DTSTART, the start date (excluding time) of the diary entry,
232%t TIMESTAMP, a unique creation timestamp, 250%t TIMESTAMP, a unique creation timestamp,
233%u USERNAME, the variable `user-login-name'. 251%u USERNAME, the value of `user-login-name'.
234 252%s (obsolete, ignored)
235For example, a value of \"%s_%h@mydomain.com\" will generate a 253
236UID code for each entry composed of the time of the event, a hash 254For example, a value of \"%h%t@mydomain.com\" will generate a UID code
237code for the event, and your personal domain name." 255for each entry composed of a hash of the event data, a creation
238 :type 'string 256timestamp, and your personal domain name."
239 :group 'icalendar) 257 :type 'string)
240 258
241(defcustom icalendar-export-sexp-enumeration-days 259(defcustom ical:vcalendar-prodid
242 14 260 (format "-//gnu.org//GNU Emacs %s//EN" emacs-version)
243 "Number of days over which a sexp diary entry is enumerated. 261 "The value of the `icalendar-prodid' property for VCALENDAR objects
244In general sexp entries cannot be translated to icalendar format. 262produced by this Emacs."
245They are therefore enumerated, i.e. explicitly evaluated for a 263 :type 'string)
246certain number of days, and then exported. The enumeration starts 264
247on the current day and continues for the number of days given here. 265(defconst ical:vcalendar-version "2.0"
248 266 "The current version of the VCALENDAR object, used in the
249See `icalendar-export-sexp-enumerate-all' for a list of sexp 267`icalendar-version' property. \"2.0\" is the version corresponding to
250entries which by default are NOT enumerated." 268RFC5545.")
251 :version "25.1" 269
252 :type 'integer 270(define-obsolete-variable-alias
253 :group 'icalendar) 271 'icalendar-export-sexp-enumeration-days
254 272 'diary-icalendar-export-sexp-enumeration-days
255(defcustom icalendar-export-sexp-enumerate-all 273 "31.1")
256 nil 274
257 "Determines whether ALL sexp diary entries are enumerated. 275(define-obsolete-variable-alias
258If non-nil all sexp diary entries are enumerated for 276 'icalendar-export-sexp-enumerate-all
259`icalendar-export-sexp-enumeration-days' days instead of 277 'diary-icalendar-export-sexp-enumerate-all
260translating into an icalendar equivalent. This affects the 278 "31.1")
261following sexp diary entries: `diary-anniversary',
262`diary-cyclic', `diary-date', `diary-float', `diary-block'. All
263other sexp entries are enumerated in any case."
264 :version "25.1"
265 :type 'boolean
266 :group 'icalendar)
267
268 279
269(defcustom icalendar-export-alarms 280(defcustom icalendar-export-alarms
270 nil 281 nil
@@ -283,19 +294,39 @@ other sexp entries are enumerated in any case."
283 (list :tag "Email" 294 (list :tag "Email"
284 (const email) 295 (const email)
285 (repeat :tag "Attendees" 296 (repeat :tag "Attendees"
286 (string :tag "Email")))))) 297 (string :tag "Email")))))))
287 :group 'icalendar)
288 298
299(make-obsolete-variable
300 'icalendar-export-alarms
301 "please use the new format in `diary-icalendar-export-alarms' instead."
302 "31.1")
303
304(defcustom icalendar-debug-level 1
305 "Minimum severity for logging iCalendar error messages.
306A value of 2 only logs errors.
307A value of 1 also logs warnings.
308A value of 0 also logs debugging information."
309 :type 'integer)
289 310
290(defvar icalendar-debug nil 311(defvar icalendar-debug nil
291 "Enable icalendar debug messages.") 312 "Enable icalendar debug messages.")
292 313
314(make-obsolete-variable
315 'icalendar-debug
316 'icalendar-debug-level
317 "31.1")
318
293;; ====================================================================== 319;; ======================================================================
294;; NO USER SERVICEABLE PARTS BELOW THIS LINE 320;; NO USER SERVICEABLE PARTS BELOW THIS LINE
295;; ====================================================================== 321;; ======================================================================
296 322
297(defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"]) 323(defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
298 324
325(make-obsolete-variable
326 'icalendar--weekday-array
327 'icalendar-weekday-numbers
328 "31.1")
329
299;; ====================================================================== 330;; ======================================================================
300;; all the other libs we need 331;; all the other libs we need
301;; ====================================================================== 332;; ======================================================================
@@ -307,8 +338,305 @@ other sexp entries are enumerated in any case."
307;; ====================================================================== 338;; ======================================================================
308(defun icalendar--dmsg (&rest args) 339(defun icalendar--dmsg (&rest args)
309 "Print message ARGS if `icalendar-debug' is non-nil." 340 "Print message ARGS if `icalendar-debug' is non-nil."
310 (if icalendar-debug 341 (declare (obsolete icalendar-warn "31.1"))
311 (apply 'message args))) 342 (if (or icalendar-debug (= 0 icalendar-debug-level))
343 (with-current-buffer (ical:error-buffer)
344 (goto-char (point-max))
345 (insert (apply #'format-message args))
346 (insert "\n"))))
347
348;; ======================================================================
349;; Error handling
350;; ======================================================================
351(define-error 'ical:error "iCalendar error")
352
353(defconst ical:error-buffer-name "*icalendar-errors*"
354 "Name of buffer in which errors are listed when processing iCalendar data.")
355
356(defun ical:error-buffer ()
357 "Return the iCalendar errors buffer, creating it if necessary.
358The buffer name is based on `icalendar-error-buffer-name'."
359 (get-buffer-create ical:error-buffer-name))
360
361(defvar ical:inhibit-error-erasure nil
362 "When non-nil, `icalendar-init-error-buffer' will not erase the errors
363buffer.")
364
365(defun ical:init-error-buffer (&optional err-buffer)
366 "Prepare ERR-BUFFER for iCalendar errors.
367ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'.
368Erases ERR-BUFFER and places it in `icalendar-errors-mode'."
369 (with-current-buffer (or err-buffer (ical:error-buffer))
370 (unless ical:inhibit-error-erasure
371 (let ((inhibit-read-only t))
372 (erase-buffer)))
373 (if (not (eq major-mode 'icalendar-errors-mode))
374 (icalendar-errors-mode))))
375
376(defun ical:errors-p (&optional err-buffer)
377 "Return non-nil if iCalendar errors have been reported in ERR-BUFFER.
378ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'."
379 (with-current-buffer (or err-buffer (ical:error-buffer))
380 (not (= (point-min) (point-max)))))
381
382(defun ical:warn (msg &rest err-plist)
383 "Write a warning to the `icalendar-error-buffer' without signaling an error."
384 (plist-put err-plist :message msg)
385 (unless (plist-get err-plist :severity)
386 (plist-put err-plist :severity 1))
387 (ical:handle-generic-error `(ical:warning . ,err-plist)))
388
389(defconst ical:error-regexp
390 (rx line-start
391 (zero-or-one
392 (group "("
393 (or (group-n 3 "ERROR") (group-n 4 "WARNING") (group-n 5 "INFO"))
394 ")"))
395 (group-n 1 (zero-or-one " *UNFOLDED:") (zero-or-more (not ":"))) ":"
396 (zero-or-one (group-n 2 (one-or-more digit)))
397 ":")
398 "Regexp to match iCalendar errors.
399
400Group 1 contains the buffer name where the error originated.
401Group 2 contains the buffer position.
402Groups 3-5 match the severity:
403 3 matches ERROR
404 4 matches WARNING
405 5 matches INFO")
406
407(cl-defun ical:format-error (&rest error-plist
408 &key (message "Unknown error")
409 severity
410 buffer
411 position
412 &allow-other-keys)
413 "Format iCalendar error data to a string.
414
415MESSAGE should be a string; it defaults to \"Unknown error\".
416BUFFER should be a buffer; POSITION should be a position in BUFFER.
417SEVERITY can be 0 for debug information, or 1 for a warning; otherwise
418a genuine error is reported.
419
420The returned error message looks like
421
422(LEVEL)BUFFER:POSITION: MESSAGE
423DEBUG-INFO...
424
425where LEVEL is derived from SEVERITY. DEBUG-INFO contains any additional
426data in ERROR-PLIST, if `icalendar-debug-level' is
4270. `icalendar-error-regexp' matches the fields in such messages."
428 (let ((name (copy-sequence (buffer-name buffer)))
429 (pos (if (integer-or-marker-p position)
430 (format "%d" position)
431 ""))
432 (level (cond ((eq severity 0) "INFO")
433 ((eq severity 1) "WARNING")
434 (t "ERROR")))
435 (debug-info (if (not (= 0 icalendar-debug-level))
436 ""
437 (mapconcat ;; (:key val...) => "Key: val\n..."
438 (lambda (val)
439 (if (keywordp val)
440 (capitalize (substring (symbol-name val) 1))
441 (format ": %s\n" val)))
442 error-plist))))
443 ;; Make sure buffer name doesn't take too much space:
444 (when (< 8 (length name))
445 (if (equal " *UNFOLDED:" (substring name 0 11))
446 (put-text-property 0 11 'display "..." name)
447 (put-text-property 9 (length name) 'display "..." name)))
448 (format "(%s)%s:%s: %s\n%s" level name pos message debug-info)))
449
450(defun ical:handle-generic-error (err-data &optional err-buffer)
451 "Log error data to ERR-BUFFER (default: the iCalendar error buffer).
452ERR-DATA should be a list (ERROR-SYMBOL . SIGNAL-DATA) where
453SIGNAL-DATA is a plist of error data."
454 (let* ((signal-data (cdr err-data))
455 (err-plist (when (plistp signal-data) signal-data))
456 (err-symbol (car err-data))
457 (severity (or (plist-get err-plist :severity) 2))
458 (buf (current-buffer)))
459 (when (<= ical:debug-level severity)
460 (with-current-buffer (or err-buffer (ical:error-buffer))
461 (goto-char (point-max))
462 (let ((inhibit-read-only t))
463 (unless (bolp) (insert "\n"))
464 (insert (apply #'ical:format-error
465 (or err-plist
466 (list :buffer buf
467 :message
468 (format "Unhandled %s error: %s"
469 err-symbol signal-data))))))))))
470
471(defmacro ical:condition-case (var bodyform &rest handlers)
472 "Like `condition-case', but with default handler for unhandled iCalendar errors.
473If none of HANDLERS handles an error, it will be handled by
474`icalendar-handle-generic-error'."
475 `(condition-case ,var
476 ,bodyform
477 ,@handlers
478 (ical:error (ical:handle-generic-error ,var))))
479
480;;; Mode based on compilation-mode for navigating error buffer:
481(defun ical:-buffer-from-error ()
482 (when-let* ((name (match-string 1)))
483 (or (get-buffer name)
484 (find-buffer-visiting name))))
485
486(defun ical:-filename-from-error ()
487 (when-let* ((buf (ical:-buffer-from-error)))
488 (buffer-file-name buf)))
489
490(defun ical:-lineno-from-error ()
491 (when-let* ((buf (ical:-buffer-from-error))
492 (posstr (match-string 2))
493 (pos (string-to-number posstr)))
494 (with-current-buffer buf
495 (line-number-at-pos pos))))
496
497(defconst ical:error-regexp-alist
498 (list (list icalendar-error-regexp
499 #'ical:-filename-from-error
500 #'ical:-lineno-from-error
501 nil
502 nil
503 nil
504 '(2 compilation-line-face)
505 '(3 compilation-error-face)
506 '(4 compilation-warning-face)
507 '(5 compilation-info-face)))
508 "Specifies how errors are parsed in `icalendar-errors-mode';
509see `compilation-error-regexp-alist'.")
510
511(define-compilation-mode ical:errors-mode "iCalendar Errors"
512 "Mode for listing and visiting errors when processing iCalendar data."
513 (setq-local compilation-error-regexp-alist ical:error-regexp-alist))
514
515;; ======================================================================
516;; UIDs
517;; ======================================================================
518(defvar ical:-uid-count 0
519 "Internal counter for creating unique ids.")
520
521(defun ical:make-uid (&optional contents _)
522 "Construct a unique ID from `icalendar-uid-format'.
523
524CONTENTS can be any object which represents the contents of the
525iCalendar component for which the UID is generated. If CONTENTS is a
526string with the text property \\='uid, that property's value will be
527used as the returned UID.
528
529Otherwise, CONTENTS will be used to create the hash substituted for
530\\='%h' in `icalendar-uid-format'. If CONTENTS is not given, the hash
531will be based on an internal counter, the system name, and the current
532time in nanoseconds.
533
534The second optional argument is for backward compatibility and is ignored."
535 (cl-incf icalendar--uid-count)
536 (let* ((uid icalendar-uid-format)
537 (timestamp (format-time-string "%s%N"))
538 (tohash (or contents
539 (format "%d%s%s" ical:-uid-count (system-name) timestamp))))
540 (if (and (stringp contents) (get-text-property 0 'uid contents))
541 ;; "Allow other apps (such as org-mode) to create its own uid"
542 ;; FIXME: is this necessary? If caller already has a UID, why
543 ;; call this function at all?
544 (setq uid (get-text-property 0 'uid contents))
545 (progn
546 (setq uid (replace-regexp-in-string
547 "%c" (format "%d" icalendar--uid-count) uid t t))
548 (setq uid (replace-regexp-in-string
549 "%t" timestamp uid t t))
550 (setq uid (replace-regexp-in-string
551 "%h" (format "%d" (abs (sxhash tohash))) uid t t))
552 (setq uid (replace-regexp-in-string
553 "%u" (or user-login-name "UNKNOWN_USER") uid t t))
554 ;; `%s' no longer used, but allowed for backward compatibility:
555 (setq uid (replace-regexp-in-string "%s" "" uid t t))))
556 uid))
557
558
559;; Essentially everything beyond this point is obsoleted by the new
560;; implementation in diary-icalendar.el. Since the functions below call
561;; each other and they still need to live in this file for now (see
562;; Bug#74994), prevent byte compiler warnings when compiling this file:
563(with-suppressed-warnings
564 ((obsolete icalendar-import-format
565 icalendar-import-format-summary
566 icalendar-import-format-description
567 icalendar-import-format-location
568 icalendar-import-format-organizer
569 icalendar-import-format-url
570 icalendar-import-format-uid
571 icalendar-import-format-status
572 icalendar-import-format-class
573 icalendar-recurring-start-year
574 icalendar-export-hidden-diary-entries
575 icalendar-export-sexp-enumeration-days
576 icalendar-export-sexp-enumerate-all
577 icalendar-export-alarms
578 icalendar-debug nil
579 icalendar--weekday-array
580 icalendar--dmsg
581 icalendar--get-unfolded-buffer
582 icalendar--clean-up-line-endings
583 icalendar--rris
584 icalendar--read-element
585 icalendar--get-event-property
586 icalendar--get-event-property-attributes
587 icalendar--get-event-properties
588 icalendar--get-children
589 icalendar--all-events
590 icalendar--split-value
591 icalendar--convert-tz-offset
592 icalendar--parse-vtimezone
593 icalendar--get-most-recent-observance
594 icalendar--convert-all-timezones
595 icalendar--find-time-zone
596 icalendar--decode-isodatetime
597 icalendar--decode-isoduration
598 icalendar--add-decoded-times
599 icalendar--datetime-to-american-date
600 icalendar--datetime-to-european-date
601 icalendar--datetime-to-iso-date
602 icalendar--datetime-to-diary-date
603 icalendar--datetime-to-colontime
604 icalendar--get-month-number
605 icalendar--get-weekday-number
606 icalendar--get-weekday-numbers
607 icalendar--get-weekday-abbrev
608 icalendar--date-to-isodate
609 icalendar--datestring-to-isodate
610 icalendar--diarytime-to-isotime
611 icalendar--convert-string-for-export
612 icalendar--convert-string-for-import
613 icalendar-export-file
614 icalendar-export-region
615 icalendar--create-uid
616 icalendar--convert-to-ical
617 icalendar--parse-summary-and-rest
618 icalendar--create-ical-alarm
619 icalendar--do-create-ical-alarm
620 icalendar--convert-ordinary-to-ical
621 icalendar-first-weekday-of-year
622 icalendar--convert-weekly-to-ical
623 icalendar--convert-yearly-to-ical
624 icalendar--convert-sexp-to-ical
625 icalendar--convert-block-to-ical
626 icalendar--convert-float-to-ical
627 icalendar--convert-date-to-ical
628 icalendar--convert-cyclic-to-ical
629 icalendar--convert-anniversary-to-ical
630 icalendar-import-file
631 icalendar-import-buffer
632 icalendar--format-ical-event
633 icalendar--convert-ical-to-diary
634 icalendar--convert-recurring-to-diary
635 icalendar--convert-non-recurring-all-day-to-diary
636 icalendar--convert-non-recurring-not-all-day-to-diary
637 icalendar--add-diary-entry
638 icalendar-import-format-sample
639 icalendar-version))
312 640
313;; ====================================================================== 641;; ======================================================================
314;; Core functionality 642;; Core functionality
@@ -321,6 +649,7 @@ Folding is the iCalendar way of wrapping long lines. In the
321created buffer all occurrences of CR LF BLANK are replaced by the 649created buffer all occurrences of CR LF BLANK are replaced by the
322empty string. Argument FOLDED-ICAL-BUFFER is the folded input 650empty string. Argument FOLDED-ICAL-BUFFER is the folded input
323buffer." 651buffer."
652 (declare (obsolete icalendar-unfolded-buffer-from-buffer "31.1"))
324 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*"))) 653 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
325 (save-current-buffer 654 (save-current-buffer
326 (set-buffer unfolded-buffer) 655 (set-buffer unfolded-buffer)
@@ -337,13 +666,14 @@ buffer."
337All occurrences of (CR LF) and (LF CF) are replaced with LF in 666All occurrences of (CR LF) and (LF CF) are replaced with LF in
338the current buffer. This is necessary in buffers which contain a 667the current buffer. This is necessary in buffers which contain a
339mix of different line endings." 668mix of different line endings."
669 (declare (obsolete nil "31.1"))
340 (save-excursion 670 (save-excursion
341 (goto-char (point-min)) 671 (goto-char (point-min))
342 (while (re-search-forward "\r\n\\|\n\r" nil t) 672 (while (re-search-forward "\r\n\\|\n\r" nil t)
343 (replace-match "\n" nil nil)))) 673 (replace-match "\n" nil nil))))
344 674
345(define-obsolete-function-alias 'icalendar--rris 675(define-obsolete-function-alias 'icalendar--rris
346 'replace-regexp-in-string "27.1") 676 #'replace-regexp-in-string "27.1")
347 677
348(defun icalendar--read-element (invalue inparams) 678(defun icalendar--read-element (invalue inparams)
349 "Recursively read the next iCalendar element in the current buffer. 679 "Recursively read the next iCalendar element in the current buffer.
@@ -352,6 +682,8 @@ INPARAMS gives the current parameters.....
352This function calls itself recursively for each nested calendar element 682This function calls itself recursively for each nested calendar element
353it finds. The current buffer should be an unfolded buffer as returned 683it finds. The current buffer should be an unfolded buffer as returned
354from `icalendar--get-unfolded-buffer'." 684from `icalendar--get-unfolded-buffer'."
685 (declare (obsolete "use `icalendar-parse' or one of `icalendar-parse-component',
686`icalendar-parse-property', `icalendar-parse-params' instead." "31.1"))
355 (let (element children line name params param param-name param-value 687 (let (element children line name params param param-name param-value
356 value 688 value
357 (continue t)) 689 (continue t))
@@ -408,6 +740,7 @@ from `icalendar--get-unfolded-buffer'."
408 740
409(defun icalendar--get-event-property (event prop) 741(defun icalendar--get-event-property (event prop)
410 "For the given EVENT return the value of the first occurrence of PROP." 742 "For the given EVENT return the value of the first occurrence of PROP."
743 (declare (obsolete icalendar-with-component "31.1"))
411 (catch 'found 744 (catch 'found
412 (let ((props (car (cddr event))) pp) 745 (let ((props (car (cddr event))) pp)
413 (while props 746 (while props
@@ -419,6 +752,7 @@ from `icalendar--get-unfolded-buffer'."
419 752
420(defun icalendar--get-event-property-attributes (event prop) 753(defun icalendar--get-event-property-attributes (event prop)
421 "For the given EVENT return attributes of the first occurrence of PROP." 754 "For the given EVENT return attributes of the first occurrence of PROP."
755 (declare (obsolete icalendar-with-component "31.1"))
422 (catch 'found 756 (catch 'found
423 (let ((props (car (cddr event))) pp) 757 (let ((props (car (cddr event))) pp)
424 (while props 758 (while props
@@ -430,6 +764,7 @@ from `icalendar--get-unfolded-buffer'."
430 764
431(defun icalendar--get-event-properties (event prop) 765(defun icalendar--get-event-properties (event prop)
432 "For the given EVENT return a list of all values of the property PROP." 766 "For the given EVENT return a list of all values of the property PROP."
767 (declare (obsolete icalendar-with-component "31.1"))
433 (let ((props (car (cddr event))) pp result) 768 (let ((props (car (cddr event))) pp result)
434 (while props 769 (while props
435 (setq pp (car props)) 770 (setq pp (car props))
@@ -456,6 +791,7 @@ from `icalendar--get-unfolded-buffer'."
456 "Return all children of the given NODE which have a name NAME. 791 "Return all children of the given NODE which have a name NAME.
457For instance the VCALENDAR node can have VEVENT children as well as VTODO 792For instance the VCALENDAR node can have VEVENT children as well as VTODO
458children." 793children."
794 (declare (obsolete icalendar-ast-node-children "31.1"))
459 (let ((result nil) 795 (let ((result nil)
460 (children (cadr (cddr node)))) 796 (children (cadr (cddr node))))
461 (when (eq (car node) name) 797 (when (eq (car node) name)
@@ -476,6 +812,7 @@ children."
476;; private 812;; private
477(defun icalendar--all-events (icalendar) 813(defun icalendar--all-events (icalendar)
478 "Return the list of all existing events in the given ICALENDAR." 814 "Return the list of all existing events in the given ICALENDAR."
815 (declare (obsolete icalendar-with-component "31.1"))
479 (let ((result '())) 816 (let ((result '()))
480 (mapc (lambda (elt) 817 (mapc (lambda (elt)
481 (setq result (append (icalendar--get-children elt 'VEVENT) 818 (setq result (append (icalendar--get-children elt 'VEVENT)
@@ -485,6 +822,7 @@ children."
485 822
486(defun icalendar--split-value (value-string) 823(defun icalendar--split-value (value-string)
487 "Split VALUE-STRING at `;='." 824 "Split VALUE-STRING at `;='."
825 (declare (obsolete nil "31.1"))
488 (let ((result '()) 826 (let ((result '())
489 param-name param-value) 827 param-name param-value)
490 (when value-string 828 (when value-string
@@ -509,6 +847,7 @@ children."
509ALIST is an alist entry from a VTIMEZONE, like STANDARD. 847ALIST is an alist entry from a VTIMEZONE, like STANDARD.
510DST-P is non-nil if this is for daylight savings time. 848DST-P is non-nil if this is for daylight savings time.
511The strings are suitable for assembling into a TZ variable." 849The strings are suitable for assembling into a TZ variable."
850 (declare (obsolete nil "31.1"))
512 (let* ((offsetto (car (cddr (assq 'TZOFFSETTO alist)))) 851 (let* ((offsetto (car (cddr (assq 'TZOFFSETTO alist))))
513 (offsetfrom (car (cddr (assq 'TZOFFSETFROM alist)))) 852 (offsetfrom (car (cddr (assq 'TZOFFSETFROM alist))))
514 (rrule-value (car (cddr (assq 'RRULE alist)))) 853 (rrule-value (car (cddr (assq 'RRULE alist))))
@@ -561,6 +900,7 @@ The strings are suitable for assembling into a TZ variable."
561 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING). 900 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
562Consider only the most recent date specification. 901Consider only the most recent date specification.
563Return nil if timezone cannot be parsed." 902Return nil if timezone cannot be parsed."
903 (declare (obsolete nil "31.1"))
564 (let* ((tz-id (icalendar--convert-string-for-import 904 (let* ((tz-id (icalendar--convert-string-for-import
565 (icalendar--get-event-property alist 'TZID))) 905 (icalendar--get-event-property alist 'TZID)))
566 (daylight (cadr (cdar (icalendar--get-most-recent-observance alist 'DAYLIGHT)))) 906 (daylight (cadr (cdar (icalendar--get-most-recent-observance alist 'DAYLIGHT))))
@@ -578,6 +918,7 @@ Return nil if timezone cannot be parsed."
578 "Return the latest observance for SUB-COMP DAYLIGHT or STANDARD. 918 "Return the latest observance for SUB-COMP DAYLIGHT or STANDARD.
579ALIST is a VTIMEZONE potentially containing historical records." 919ALIST is a VTIMEZONE potentially containing historical records."
580;FIXME?: "most recent" should be relative to a given date 920;FIXME?: "most recent" should be relative to a given date
921 (declare (obsolete icalendar-recur-tz-observance-on "31.1"))
581 (let ((components (icalendar--get-children alist sub-comp))) 922 (let ((components (icalendar--get-children alist sub-comp)))
582 (list 923 (list
583 (car 924 (car
@@ -591,7 +932,7 @@ ALIST is a VTIMEZONE potentially containing historical records."
591 (and (memq (car p) '(DTSTART RDATE)) 932 (and (memq (car p) '(DTSTART RDATE))
592 (car (cddr p)))) 933 (car (cddr p))))
593 n)) 934 n))
594 'string-greaterp)))) 935 #'string-greaterp))))
595 (a-recent (funcall get-recent (car (cddr a)))) 936 (a-recent (funcall get-recent (car (cddr a))))
596 (b-recent (funcall get-recent (car (cddr b))))) 937 (b-recent (funcall get-recent (car (cddr b)))))
597 (string-greaterp a-recent b-recent)))))))) 938 (string-greaterp a-recent b-recent))))))))
@@ -600,6 +941,7 @@ ALIST is a VTIMEZONE potentially containing historical records."
600 "Convert all timezones in the ICALENDAR into an alist. 941 "Convert all timezones in the ICALENDAR into an alist.
601Each element of the alist is a cons (ID . TZ-STRING), 942Each element of the alist is a cons (ID . TZ-STRING),
602like `icalendar--parse-vtimezone'." 943like `icalendar--parse-vtimezone'."
944 (declare (obsolete nil "31.1"))
603 (let (result) 945 (let (result)
604 (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE)) 946 (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE))
605 (setq zone (icalendar--parse-vtimezone zone)) 947 (setq zone (icalendar--parse-vtimezone zone))
@@ -610,6 +952,7 @@ like `icalendar--parse-vtimezone'."
610(defun icalendar--find-time-zone (prop-list zone-map) 952(defun icalendar--find-time-zone (prop-list zone-map)
611 "Return a timezone string for the time zone in PROP-LIST, or nil if none. 953 "Return a timezone string for the time zone in PROP-LIST, or nil if none.
612ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'." 954ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
955 (declare (obsolete nil "31.1"))
613 (let ((id (plist-get prop-list 'TZID))) 956 (let ((id (plist-get prop-list 'TZID)))
614 (if id 957 (if id
615 (cdr (assoc id zone-map))))) 958 (cdr (assoc id zone-map)))))
@@ -628,6 +971,7 @@ in any format understood by `encode-time'.
628RESULT-ZONE, if provided, is the timezone for encoding the result 971RESULT-ZONE, if provided, is the timezone for encoding the result
629in any format understood by `decode-time'. 972in any format understood by `decode-time'.
630FIXME: multiple comma-separated values should be allowed!" 973FIXME: multiple comma-separated values should be allowed!"
974 (declare (obsolete icalendar-read-date-time "31.1"))
631 (icalendar--dmsg isodatetimestring) 975 (icalendar--dmsg isodatetimestring)
632 (if isodatetimestring 976 (if isodatetimestring
633 ;; day/month/year must be present 977 ;; day/month/year must be present
@@ -685,6 +1029,7 @@ Optional argument DURATION-CORRECTION shortens result by one day.
685 1029
686FIXME: TZID-attributes are ignored....! 1030FIXME: TZID-attributes are ignored....!
687FIXME: multiple comma-separated values should be allowed!" 1031FIXME: multiple comma-separated values should be allowed!"
1032 (declare (obsolete icalendar-read-dur-value "31.1"))
688 (if isodurationstring 1033 (if isodurationstring
689 (save-match-data 1034 (save-match-data
690 (string-match 1035 (string-match
@@ -740,6 +1085,7 @@ FIXME: multiple comma-separated values should be allowed!"
740 "Add TIME1 to TIME2. 1085 "Add TIME1 to TIME2.
741Both times must be given in decoded form. One of these times must be 1086Both times must be given in decoded form. One of these times must be
742valid (year > 1900 or something)." 1087valid (year > 1900 or something)."
1088 (declare (obsolete icalendar-date-time-add "31.1"))
743 ;; FIXME: does this function exist already? Can we use decoded-time-add? 1089 ;; FIXME: does this function exist already? Can we use decoded-time-add?
744 (decode-time (encode-time 1090 (decode-time (encode-time
745 ;; FIXME: Support subseconds. 1091 ;; FIXME: Support subseconds.
@@ -761,6 +1107,8 @@ valid (year > 1900 or something)."
761Optional argument SEPARATOR gives the separator between month, 1107Optional argument SEPARATOR gives the separator between month,
762day, and year. If nil a blank character is used as separator. 1108day, and year. If nil a blank character is used as separator.
763American format: \"month day year\"." 1109American format: \"month day year\"."
1110 (declare (obsolete "use `icalendar-date/time-to-date' and
1111`diary-icalendar-format-date' instead." "31.1"))
764 (if datetime 1112 (if datetime
765 (format "%d%s%d%s%d" (nth 4 datetime) ;month 1113 (format "%d%s%d%s%d" (nth 4 datetime) ;month
766 (or separator " ") 1114 (or separator " ")
@@ -776,6 +1124,7 @@ Optional argument SEPARATOR gives the separator between month,
776day, and year. If nil a blank character is used as separator. 1124day, and year. If nil a blank character is used as separator.
777European format: (day month year). 1125European format: (day month year).
778FIXME" 1126FIXME"
1127 (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
779 (if datetime 1128 (if datetime
780 (format "%d%s%d%s%d" (nth 3 datetime) ;day 1129 (format "%d%s%d%s%d" (nth 3 datetime) ;day
781 (or separator " ") 1130 (or separator " ")
@@ -790,6 +1139,7 @@ FIXME"
790Optional argument SEPARATOR gives the separator between month, 1139Optional argument SEPARATOR gives the separator between month,
791day, and year. If nil a blank character is used as separator. 1140day, and year. If nil a blank character is used as separator.
792ISO format: (year month day)." 1141ISO format: (year month day)."
1142 (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
793 (if datetime 1143 (if datetime
794 (format "%d%s%d%s%d" (nth 5 datetime) ;year 1144 (format "%d%s%d%s%d" (nth 5 datetime) ;year
795 (or separator " ") 1145 (or separator " ")
@@ -805,6 +1155,7 @@ Optional argument SEPARATOR gives the separator between month,
805day, and year. If nil a blank character is used as separator. 1155day, and year. If nil a blank character is used as separator.
806Call icalendar--datetime-to-*-date according to the current 1156Call icalendar--datetime-to-*-date according to the current
807calendar date style." 1157calendar date style."
1158 (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
808 (funcall (intern-soft (format "icalendar--datetime-to-%s-date" 1159 (funcall (intern-soft (format "icalendar--datetime-to-%s-date"
809 calendar-date-style)) 1160 calendar-date-style))
810 datetime separator)) 1161 datetime separator))
@@ -812,10 +1163,12 @@ calendar date style."
812(defun icalendar--datetime-to-colontime (datetime) 1163(defun icalendar--datetime-to-colontime (datetime)
813 "Extract the time part of a decoded DATETIME into 24-hour format. 1164 "Extract the time part of a decoded DATETIME into 24-hour format.
814Note that this silently ignores seconds." 1165Note that this silently ignores seconds."
1166 (declare (obsolete diary-icalendar-format-time "31.1"))
815 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime))) 1167 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))
816 1168
817(defun icalendar--get-month-number (monthname) 1169(defun icalendar--get-month-number (monthname)
818 "Return the month number for the given MONTHNAME." 1170 "Return the month number for the given MONTHNAME."
1171 (declare (obsolete nil "31.1"))
819 (catch 'found 1172 (catch 'found
820 (let ((num 1) 1173 (let ((num 1)
821 (m (downcase monthname))) 1174 (m (downcase monthname)))
@@ -831,6 +1184,7 @@ Note that this silently ignores seconds."
831 1184
832(defun icalendar--get-weekday-number (abbrevweekday) 1185(defun icalendar--get-weekday-number (abbrevweekday)
833 "Return the number for the ABBREVWEEKDAY." 1186 "Return the number for the ABBREVWEEKDAY."
1187 (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
834 (if abbrevweekday 1188 (if abbrevweekday
835 (catch 'found 1189 (catch 'found
836 (let ((num 0) 1190 (let ((num 0)
@@ -846,6 +1200,7 @@ Note that this silently ignores seconds."
846 1200
847(defun icalendar--get-weekday-numbers (abbrevweekdays) 1201(defun icalendar--get-weekday-numbers (abbrevweekdays)
848 "Return the list of numbers for the comma-separated ABBREVWEEKDAYS." 1202 "Return the list of numbers for the comma-separated ABBREVWEEKDAYS."
1203 (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
849 (when abbrevweekdays 1204 (when abbrevweekdays
850 (let* ((num -1) 1205 (let* ((num -1)
851 (weekday-alist (mapcar (lambda (day) 1206 (weekday-alist (mapcar (lambda (day)
@@ -860,6 +1215,7 @@ Note that this silently ignores seconds."
860 1215
861(defun icalendar--get-weekday-abbrev (weekday) 1216(defun icalendar--get-weekday-abbrev (weekday)
862 "Return the abbreviated WEEKDAY." 1217 "Return the abbreviated WEEKDAY."
1218 (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
863 (catch 'found 1219 (catch 'found
864 (let ((num 0) 1220 (let ((num 0)
865 (w (downcase weekday))) 1221 (w (downcase weekday)))
@@ -877,6 +1233,7 @@ Note that this silently ignores seconds."
877 "Convert DATE to iso-style date. 1233 "Convert DATE to iso-style date.
878DATE must be a list of the form (month day year). 1234DATE must be a list of the form (month day year).
879If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days." 1235If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
1236 (declare (obsolete icalendar-print-date "31.1"))
880 (let ((mdy (calendar-gregorian-from-absolute 1237 (let ((mdy (calendar-gregorian-from-absolute
881 (+ (calendar-absolute-from-gregorian date) 1238 (+ (calendar-absolute-from-gregorian date)
882 (or day-shift 0))))) 1239 (or day-shift 0)))))
@@ -891,6 +1248,7 @@ non-nil, the result is shifted by YEAR-SHIFT years -- YEAR-SHIFT
891must be either nil or an integer. This function tries to figure 1248must be either nil or an integer. This function tries to figure
892the date style from DATESTRING itself. If that is not possible 1249the date style from DATESTRING itself. If that is not possible
893it uses the current calendar date style." 1250it uses the current calendar date style."
1251 (declare (obsolete "use `diary-icalendar-parse-date-form' and `icalendar-print-date' instead." "31.1"))
894 (let ((day -1) month year) 1252 (let ((day -1) month year)
895 (save-match-data 1253 (save-match-data
896 (cond ( ;; iso-style numeric date 1254 (cond ( ;; iso-style numeric date
@@ -981,6 +1339,7 @@ In this example the TIMESTRING would be \"9:30\" and the
981AMPMSTRING would be \"pm\". The minutes may be missing as long 1339AMPMSTRING would be \"pm\". The minutes may be missing as long
982as the colon is missing as well, i.e. \"9\" is allowed as 1340as the colon is missing as well, i.e. \"9\" is allowed as
983TIMESTRING and has the same result as \"9:00\"." 1341TIMESTRING and has the same result as \"9:00\"."
1342 (declare (obsolete "use `diary-icalendar-parse-time' and `icalendar-print-date-time' instead." "31.1"))
984 (if timestring 1343 (if timestring
985 (let* ((parts (save-match-data (split-string timestring ":"))) 1344 (let* ((parts (save-match-data (split-string timestring ":")))
986 (h (car parts)) 1345 (h (car parts))
@@ -1018,20 +1377,19 @@ TIMESTRING and has the same result as \"9:00\"."
1018 "Export diary file to iCalendar format. 1377 "Export diary file to iCalendar format.
1019All diary entries in the file DIARY-FILENAME are converted to iCalendar 1378All diary entries in the file DIARY-FILENAME are converted to iCalendar
1020format. The result is appended to the file ICAL-FILENAME." 1379format. The result is appended to the file ICAL-FILENAME."
1380 (declare (obsolete diary-icalendar-export-file "31.1"))
1021 (interactive "FExport diary data from file: \n\ 1381 (interactive "FExport diary data from file: \n\
1022Finto iCalendar file: ") 1382Finto iCalendar file: ")
1023 (save-current-buffer 1383 (save-current-buffer
1024 (set-buffer (find-file diary-filename)) 1384 (set-buffer (find-file diary-filename))
1025 (icalendar-export-region (point-min) (point-max) ical-filename))) 1385 (icalendar-export-region (point-min) (point-max) ical-filename)))
1026 1386
1027(defvar icalendar--uid-count 0
1028 "Auxiliary counter for creating unique ids.")
1029
1030(defun icalendar--create-uid (entry-full contents) 1387(defun icalendar--create-uid (entry-full contents)
1031 "Construct a unique iCalendar UID for a diary entry. 1388 "Construct a unique iCalendar UID for a diary entry.
1032ENTRY-FULL is the full diary entry string. CONTENTS is the 1389ENTRY-FULL is the full diary entry string. CONTENTS is the
1033current iCalendar object, as a string. Increase 1390current iCalendar object, as a string. Increase
1034`icalendar--uid-count'. Returns the UID string." 1391`icalendar--uid-count'. Returns the UID string."
1392 (declare (obsolete icalendar-make-uid "31.1"))
1035 (let ((uid icalendar-uid-format)) 1393 (let ((uid icalendar-uid-format))
1036 (if 1394 (if
1037 ;; Allow other apps (such as org-mode) to create its own uid 1395 ;; Allow other apps (such as org-mode) to create its own uid
@@ -1055,7 +1413,6 @@ current iCalendar object, as a string. Increase
1055 (substring contents (match-beginning 1) (match-end 1)) 1413 (substring contents (match-beginning 1) (match-end 1))
1056 "DTSTART"))) 1414 "DTSTART")))
1057 (setq uid (replace-regexp-in-string "%s" dtstart uid t t)))) 1415 (setq uid (replace-regexp-in-string "%s" dtstart uid t t))))
1058
1059 ;; Return the UID string 1416 ;; Return the UID string
1060 uid)) 1417 uid))
1061 1418
@@ -1068,6 +1425,7 @@ ICAL-FILENAME.
1068This function attempts to return t if something goes wrong. In this 1425This function attempts to return t if something goes wrong. In this
1069case an error string which describes all the errors and problems is 1426case an error string which describes all the errors and problems is
1070written into the buffer `*icalendar-errors*'." 1427written into the buffer `*icalendar-errors*'."
1428 (declare (obsolete diary-icalendar-export-region "31.1"))
1071 (interactive "r 1429 (interactive "r
1072FExport diary data into iCalendar file: ") 1430FExport diary data into iCalendar file: ")
1073 (let ((result "") 1431 (let ((result "")
@@ -1179,6 +1537,7 @@ FExport diary data into iCalendar file: ")
1179 "Convert a diary entry to iCalendar format. 1537 "Convert a diary entry to iCalendar format.
1180NONMARKER is a regular expression matching the start of non-marking 1538NONMARKER is a regular expression matching the start of non-marking
1181entries. ENTRY-MAIN is the first line of the diary entry." 1539entries. ENTRY-MAIN is the first line of the diary entry."
1540 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1182 (or 1541 (or
1183 (unless icalendar-export-sexp-enumerate-all 1542 (unless icalendar-export-sexp-enumerate-all
1184 (or 1543 (or
@@ -1208,6 +1567,7 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1208(defun icalendar--parse-summary-and-rest (summary-and-rest) 1567(defun icalendar--parse-summary-and-rest (summary-and-rest)
1209 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties. 1568 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
1210Returns an alist." 1569Returns an alist."
1570 (declare (obsolete diary-icalendar-parse-entry "31.1"))
1211 (save-match-data 1571 (save-match-data
1212 (if (functionp icalendar-import-format) 1572 (if (functionp icalendar-import-format)
1213 ;; can't do anything 1573 ;; can't do anything
@@ -1223,7 +1583,7 @@ Returns an alist."
1223 (p-sta (or (string-match "%t" icalendar-import-format) -1)) 1583 (p-sta (or (string-match "%t" icalendar-import-format) -1))
1224 (p-url (or (string-match "%u" icalendar-import-format) -1)) 1584 (p-url (or (string-match "%u" icalendar-import-format) -1))
1225 (p-uid (or (string-match "%U" icalendar-import-format) -1)) 1585 (p-uid (or (string-match "%U" icalendar-import-format) -1))
1226 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) '<)) 1586 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) #'<))
1227 (ct 0) 1587 (ct 0)
1228 pos-cla pos-des pos-loc pos-org pos-sta pos-url pos-uid) ;pos-sum 1588 pos-cla pos-des pos-loc pos-org pos-sta pos-url pos-uid) ;pos-sum
1229 (dotimes (i (length p-list)) 1589 (dotimes (i (length p-list))
@@ -1322,6 +1682,7 @@ Returns an alist."
1322 1682
1323(defun icalendar--create-ical-alarm (summary) 1683(defun icalendar--create-ical-alarm (summary)
1324 "Return VALARM blocks for the given SUMMARY." 1684 "Return VALARM blocks for the given SUMMARY."
1685 (declare (obsolete diary-icalendar-add-valarms "31.1"))
1325 (when icalendar-export-alarms 1686 (when icalendar-export-alarms
1326 (let* ((advance-time (car icalendar-export-alarms)) 1687 (let* ((advance-time (car icalendar-export-alarms))
1327 (alarm-specs (cadr icalendar-export-alarms)) 1688 (alarm-specs (cadr icalendar-export-alarms))
@@ -1337,6 +1698,7 @@ is a list which must be one of (audio), (display) or
1337\(email (ADDRESS1 ...)), see `icalendar-export-alarms'. Argument 1698\(email (ADDRESS1 ...)), see `icalendar-export-alarms'. Argument
1338SUMMARY is a string which contains a short description for the 1699SUMMARY is a string which contains a short description for the
1339alarm." 1700alarm."
1701 (declare (obsolete diary-icalendar-add-valarms "31.1"))
1340 (let* ((action (car alarm-spec)) 1702 (let* ((action (car alarm-spec))
1341 (act (format "\nACTION:%s" 1703 (act (format "\nACTION:%s"
1342 (cdr (assoc action '((audio . "AUDIO") 1704 (cdr (assoc action '((audio . "AUDIO")
@@ -1362,6 +1724,7 @@ alarm."
1362 "Convert \"ordinary\" diary entry to iCalendar format. 1724 "Convert \"ordinary\" diary entry to iCalendar format.
1363NONMARKER is a regular expression matching the start of non-marking 1725NONMARKER is a regular expression matching the start of non-marking
1364entries. ENTRY-MAIN is the first line of the diary entry." 1726entries. ENTRY-MAIN is the first line of the diary entry."
1727 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1365 (if (string-match 1728 (if (string-match
1366 (concat nonmarker 1729 (concat nonmarker
1367 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date 1730 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date
@@ -1445,6 +1808,7 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1445(defun icalendar-first-weekday-of-year (abbrevweekday year) 1808(defun icalendar-first-weekday-of-year (abbrevweekday year)
1446 "Find the first ABBREVWEEKDAY in a given YEAR. 1809 "Find the first ABBREVWEEKDAY in a given YEAR.
1447Returns day number." 1810Returns day number."
1811 (declare (obsolete icalendar-nth-weekday-in "31.1"))
1448 (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year))) 1812 (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year)))
1449 (result (+ 1 1813 (result (+ 1
1450 (- (icalendar--get-weekday-number abbrevweekday) 1814 (- (icalendar--get-weekday-number abbrevweekday)
@@ -1459,6 +1823,7 @@ Returns day number."
1459 "Convert weekly diary entry to iCalendar format. 1823 "Convert weekly diary entry to iCalendar format.
1460NONMARKER is a regular expression matching the start of non-marking 1824NONMARKER is a regular expression matching the start of non-marking
1461entries. ENTRY-MAIN is the first line of the diary entry." 1825entries. ENTRY-MAIN is the first line of the diary entry."
1826 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1462 (if (and (string-match (concat nonmarker 1827 (if (and (string-match (concat nonmarker
1463 "\\([a-z]+\\)\\s-+" 1828 "\\([a-z]+\\)\\s-+"
1464 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)" 1829 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)"
@@ -1541,6 +1906,7 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1541 "Convert yearly diary entry to iCalendar format. 1906 "Convert yearly diary entry to iCalendar format.
1542NONMARKER is a regular expression matching the start of non-marking 1907NONMARKER is a regular expression matching the start of non-marking
1543entries. ENTRY-MAIN is the first line of the diary entry." 1908entries. ENTRY-MAIN is the first line of the diary entry."
1909 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1544 (if (string-match (concat nonmarker 1910 (if (string-match (concat nonmarker
1545 (if (eq calendar-date-style 'european) 1911 (if (eq calendar-date-style 'european)
1546 "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+" 1912 "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
@@ -1626,6 +1992,7 @@ ENTRY-MAIN is the first line of the diary entry.
1626 1992
1627Optional argument START determines the first day of the 1993Optional argument START determines the first day of the
1628enumeration, given as a Lisp time value -- used for test purposes." 1994enumeration, given as a Lisp time value -- used for test purposes."
1995 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1629 (cond ((string-match (concat nonmarker 1996 (cond ((string-match (concat nonmarker
1630 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$") 1997 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1631 entry-main) 1998 entry-main)
@@ -1678,6 +2045,7 @@ enumeration, given as a Lisp time value -- used for test purposes."
1678 "Convert block diary entry to iCalendar format. 2045 "Convert block diary entry to iCalendar format.
1679NONMARKER is a regular expression matching the start of non-marking 2046NONMARKER is a regular expression matching the start of non-marking
1680entries. ENTRY-MAIN is the first line of the diary entry." 2047entries. ENTRY-MAIN is the first line of the diary entry."
2048 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1681 (if (string-match (concat nonmarker 2049 (if (string-match (concat nonmarker
1682 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)" 2050 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1683 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*" 2051 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
@@ -1753,10 +2121,11 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1753(defun icalendar--convert-float-to-ical (nonmarker entry-main) 2121(defun icalendar--convert-float-to-ical (nonmarker entry-main)
1754 "Convert float diary entry to iCalendar format -- partially unsupported! 2122 "Convert float diary entry to iCalendar format -- partially unsupported!
1755 2123
1756 FIXME! DAY from `diary-float' yet unimplemented. 2124 FIXME! DAY from `diary-float' yet unimplemented.
1757 2125
1758 NONMARKER is a regular expression matching the start of non-marking 2126 NONMARKER is a regular expression matching the start of non-marking
1759 entries. ENTRY-MAIN is the first line of the diary entry." 2127 entries. ENTRY-MAIN is the first line of the diary entry."
2128 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1760 (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main) 2129 (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main)
1761 (with-temp-buffer 2130 (with-temp-buffer
1762 (insert (match-string 1 entry-main)) 2131 (insert (match-string 1 entry-main))
@@ -1817,6 +2186,7 @@ FIXME!
1817 2186
1818NONMARKER is a regular expression matching the start of non-marking 2187NONMARKER is a regular expression matching the start of non-marking
1819entries. ENTRY-MAIN is the first line of the diary entry." 2188entries. ENTRY-MAIN is the first line of the diary entry."
2189 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1820 (if (string-match (concat nonmarker 2190 (if (string-match (concat nonmarker
1821 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$") 2191 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1822 entry-main) 2192 entry-main)
@@ -1830,6 +2200,7 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1830 "Convert `diary-cyclic' diary entry to iCalendar format. 2200 "Convert `diary-cyclic' diary entry to iCalendar format.
1831NONMARKER is a regular expression matching the start of non-marking 2201NONMARKER is a regular expression matching the start of non-marking
1832entries. ENTRY-MAIN is the first line of the diary entry." 2202entries. ENTRY-MAIN is the first line of the diary entry."
2203 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1833 (if (string-match (concat nonmarker 2204 (if (string-match (concat nonmarker
1834 "%%(diary-cyclic \\([^ ]+\\) +" 2205 "%%(diary-cyclic \\([^ ]+\\) +"
1835 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*" 2206 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
@@ -1904,6 +2275,7 @@ entries. ENTRY-MAIN is the first line of the diary entry."
1904 "Convert `diary-anniversary' diary entry to iCalendar format. 2275 "Convert `diary-anniversary' diary entry to iCalendar format.
1905NONMARKER is a regular expression matching the start of non-marking 2276NONMARKER is a regular expression matching the start of non-marking
1906entries. ENTRY-MAIN is the first line of the diary entry." 2277entries. ENTRY-MAIN is the first line of the diary entry."
2278 (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
1907 (if (string-match (concat nonmarker 2279 (if (string-match (concat nonmarker
1908 "%%(diary-anniversary \\([^)]+\\))\\s-*" 2280 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1909 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?" 2281 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
@@ -1986,6 +2358,7 @@ Argument ICAL-FILENAME output iCalendar file.
1986Argument DIARY-FILENAME input `diary-file'. 2358Argument DIARY-FILENAME input `diary-file'.
1987Optional argument NON-MARKING determines whether events are created as 2359Optional argument NON-MARKING determines whether events are created as
1988non-marking or not." 2360non-marking or not."
2361 (declare (obsolete diary-icalendar-import-file "31.1"))
1989 (interactive "fImport iCalendar data from file: \nFInto diary file: \nP") 2362 (interactive "fImport iCalendar data from file: \nFInto diary file: \nP")
1990 ;; clean up the diary file 2363 ;; clean up the diary file
1991 (save-current-buffer 2364 (save-current-buffer
@@ -2012,6 +2385,7 @@ non-marking.
2012Return code t means that importing worked well, return code nil 2385Return code t means that importing worked well, return code nil
2013means that an error has occurred. Error messages will be in the 2386means that an error has occurred. Error messages will be in the
2014buffer `*icalendar-errors*'." 2387buffer `*icalendar-errors*'."
2388 (declare (obsolete diary-icalendar-import-buffer "31.1"))
2015 (interactive) 2389 (interactive)
2016 (save-current-buffer 2390 (save-current-buffer
2017 ;; prepare ical 2391 ;; prepare ical
@@ -2048,6 +2422,7 @@ buffer `*icalendar-errors*'."
2048 2422
2049(defun icalendar--format-ical-event (event) 2423(defun icalendar--format-ical-event (event)
2050 "Create a string representation of an iCalendar EVENT." 2424 "Create a string representation of an iCalendar EVENT."
2425 (declare (obsolete diary-icalendar-format-entry "31.1"))
2051 (if (functionp icalendar-import-format) 2426 (if (functionp icalendar-import-format)
2052 (funcall icalendar-import-format event) 2427 (funcall icalendar-import-format event)
2053 (let ((string icalendar-import-format) 2428 (let ((string icalendar-import-format)
@@ -2093,6 +2468,7 @@ events are created as non-marking.
2093This function attempts to return t if something goes wrong. In this 2468This function attempts to return t if something goes wrong. In this
2094case an error string which describes all the errors and problems is 2469case an error string which describes all the errors and problems is
2095written into the buffer `*icalendar-errors*'." 2470written into the buffer `*icalendar-errors*'."
2471 (declare (obsolete diary-icalendar-import-buffer "31.1"))
2096 (let* ((ev (icalendar--all-events ical-list)) 2472 (let* ((ev (icalendar--all-events ical-list))
2097 (error-string "") 2473 (error-string "")
2098 (event-ok t) 2474 (event-ok t)
@@ -2255,6 +2631,7 @@ written into the buffer `*icalendar-errors*'."
2255DTSTART-DEC is the DTSTART property of E. 2631DTSTART-DEC is the DTSTART property of E.
2256START-T is the event's start time in diary format. 2632START-T is the event's start time in diary format.
2257END-T is the event's end time in diary format." 2633END-T is the event's end time in diary format."
2634 (declare (obsolete diary-icalendar-format-entry "31.1"))
2258 (icalendar--dmsg "recurring event") 2635 (icalendar--dmsg "recurring event")
2259 (let* ((rrule (icalendar--get-event-property e 'RRULE)) 2636 (let* ((rrule (icalendar--get-event-property e 'RRULE))
2260 (rrule-props (icalendar--split-value rrule)) 2637 (rrule-props (icalendar--split-value rrule))
@@ -2492,6 +2869,7 @@ END-T is the event's end time in diary format."
2492DTSTART is the decoded DTSTART property of E. 2869DTSTART is the decoded DTSTART property of E.
2493Argument START-D gives the first day. 2870Argument START-D gives the first day.
2494Argument END-D gives the last day." 2871Argument END-D gives the last day."
2872 (declare (obsolete diary-icalendar-format-time-range "31.1"))
2495 (icalendar--dmsg "non-recurring all-day event") 2873 (icalendar--dmsg "non-recurring all-day event")
2496 (format "%%%%(and (diary-block %s %s))" start-d end-d)) 2874 (format "%%%%(and (diary-block %s %s))" start-d end-d))
2497 2875
@@ -2503,6 +2881,7 @@ Argument END-D gives the last day."
2503DTSTART-DEC is the decoded DTSTART property of E. 2881DTSTART-DEC is the decoded DTSTART property of E.
2504START-T is the event's start time in diary format. 2882START-T is the event's start time in diary format.
2505END-T is the event's end time in diary format." 2883END-T is the event's end time in diary format."
2884 (declare (obsolete diary-icalendar-format-time-range "31.1"))
2506 (icalendar--dmsg "not all day event") 2885 (icalendar--dmsg "not all day event")
2507 (cond (end-t 2886 (cond (end-t
2508 (format "%s %s-%s" 2887 (format "%s %s-%s"
@@ -2523,6 +2902,8 @@ determines whether diary events are created as non-marking. If
2523SUMMARY is not nil it must be a string that gives the summary of the 2902SUMMARY is not nil it must be a string that gives the summary of the
2524entry. In this case the user will be asked whether he wants to insert 2903entry. In this case the user will be asked whether he wants to insert
2525the entry." 2904the entry."
2905 (declare (obsolete "see `diary-icalendar-post-entry-format-hook' and
2906`diary-icalendar--entry-import'" "31.1"))
2526 (when (or (not summary) 2907 (when (or (not summary)
2527 (y-or-n-p (format-message "Add appointment for `%s' to diary? " 2908 (y-or-n-p (format-message "Add appointment for `%s' to diary? "
2528 summary))) 2909 summary)))
@@ -2541,6 +2922,7 @@ the entry."
2541;; ====================================================================== 2922;; ======================================================================
2542(defun icalendar-import-format-sample (event) 2923(defun icalendar-import-format-sample (event)
2543 "Example function for formatting an iCalendar EVENT." 2924 "Example function for formatting an iCalendar EVENT."
2925 (declare (obsolete "see `diary-icalendar-vevent-skeleton'" "31.1"))
2544 (format (concat "SUMMARY='%s' DESCRIPTION='%s' LOCATION='%s' ORGANIZER='%s' " 2926 (format (concat "SUMMARY='%s' DESCRIPTION='%s' LOCATION='%s' ORGANIZER='%s' "
2545 "STATUS='%s' URL='%s' CLASS='%s'") 2927 "STATUS='%s' URL='%s' CLASS='%s'")
2546 (or (icalendar--get-event-property event 'SUMMARY) "") 2928 (or (icalendar--get-event-property event 'SUMMARY) "")
@@ -2551,6 +2933,8 @@ the entry."
2551 (or (icalendar--get-event-property event 'URL) "") 2933 (or (icalendar--get-event-property event 'URL) "")
2552 (or (icalendar--get-event-property event 'CLASS) ""))) 2934 (or (icalendar--get-event-property event 'CLASS) "")))
2553 2935
2936) ; Closes the top-level `with-suppressed-warnings' form above
2937
2554;; Obsolete 2938;; Obsolete
2555 2939
2556(defconst icalendar-version "0.19" "Version number of icalendar.el.") 2940(defconst icalendar-version "0.19" "Version number of icalendar.el.")
@@ -2558,4 +2942,7 @@ the entry."
2558 2942
2559(provide 'icalendar) 2943(provide 'icalendar)
2560 2944
2945;; Local Variables:
2946;; read-symbol-shorthands: (("ical:" . "icalendar-"))
2947;; End:
2561;;; icalendar.el ends here 2948;;; icalendar.el ends here
diff --git a/lisp/calendar/lunar.el b/lisp/calendar/lunar.el
index 1f7107b1037..e20cf52013e 100644
--- a/lisp/calendar/lunar.el
+++ b/lisp/calendar/lunar.el
@@ -150,7 +150,7 @@ remainder mod 4 gives the phase: 0 new moon, 1 first quarter, 2 full moon,
150 (time (* 24 (- date (truncate date)))) 150 (time (* 24 (- date (truncate date))))
151 (date (calendar-gregorian-from-absolute (truncate date))) 151 (date (calendar-gregorian-from-absolute (truncate date)))
152 (adj (dst-adjust-time date time))) 152 (adj (dst-adjust-time date time)))
153 (list (car adj) (apply 'solar-time-string (cdr adj)) phase eclipse))) 153 (list (car adj) (apply #'solar-time-string (cdr adj)) phase eclipse)))
154 154
155;; from "Astronomy with your Personal Computer", Subroutine Eclipse 155;; from "Astronomy with your Personal Computer", Subroutine Eclipse
156;; Line 7000 Peter Duffett-Smith Cambridge University Press 1990 156;; Line 7000 Peter Duffett-Smith Cambridge University Press 1990
diff --git a/lisp/calendar/solar.el b/lisp/calendar/solar.el
index bb3d5cc1546..eeba372e69c 100644
--- a/lisp/calendar/solar.el
+++ b/lisp/calendar/solar.el
@@ -668,7 +668,7 @@ Optional NOLOCATION non-nil means do not print the location."
668 (concat "sunset " (apply #'solar-time-string (cadr l))) 668 (concat "sunset " (apply #'solar-time-string (cadr l)))
669 "no sunset") 669 "no sunset")
670 (if nolocation "" 670 (if nolocation ""
671 (format " at %s" (eval calendar-location-name))) 671 (format " at %s" (eval calendar-location-name t)))
672 (nth 2 l)))) 672 (nth 2 l))))
673 673
674(defconst solar-data-list 674(defconst solar-data-list
@@ -881,7 +881,7 @@ Accurate to a few seconds."
881 (last (calendar-last-day-of-month month year)) 881 (last (calendar-last-day-of-month month year))
882 (title (format "Sunrise/sunset times for %s %d at %s" 882 (title (format "Sunrise/sunset times for %s %d at %s"
883 (calendar-month-name month) year 883 (calendar-month-name month) year
884 (eval calendar-location-name)))) 884 (eval calendar-location-name t))))
885 (calendar-in-read-only-buffer solar-sunrises-buffer 885 (calendar-in-read-only-buffer solar-sunrises-buffer
886 (calendar-set-mode-line title) 886 (calendar-set-mode-line title)
887 (insert title ":\n\n") 887 (insert title ":\n\n")
diff --git a/lisp/calendar/timeclock.el b/lisp/calendar/timeclock.el
index 8afecb19cfa..acdf99f77ae 100644
--- a/lisp/calendar/timeclock.el
+++ b/lisp/calendar/timeclock.el
@@ -296,7 +296,7 @@ set before switching this mode on."
296`timeclock-use-display-time' to see timeclock information")) 296`timeclock-use-display-time' to see timeclock information"))
297 (add-hook 'display-time-hook #'timeclock-update-mode-line)) 297 (add-hook 'display-time-hook #'timeclock-update-mode-line))
298 (setq timeclock-update-timer 298 (setq timeclock-update-timer
299 (run-at-time nil 60 'timeclock-update-mode-line)))) 299 (run-at-time nil 60 #'timeclock-update-mode-line))))
300 (setq global-mode-string 300 (setq global-mode-string
301 (delq 'timeclock-mode-string global-mode-string)) 301 (delq 'timeclock-mode-string global-mode-string))
302 (remove-hook 'timeclock-event-hook #'timeclock-update-mode-line) 302 (remove-hook 'timeclock-event-hook #'timeclock-update-mode-line)
@@ -513,8 +513,8 @@ non-nil, the amount returned will be relative to past time worked."
513 (message "%s" string) 513 (message "%s" string)
514 string))) 514 string)))
515 515
516(define-obsolete-function-alias 'timeclock-time-to-seconds 'float-time "26.1") 516(define-obsolete-function-alias 'timeclock-time-to-seconds #'float-time "26.1")
517(define-obsolete-function-alias 'timeclock-seconds-to-time 'time-convert "26.1") 517(define-obsolete-function-alias 'timeclock-seconds-to-time #'time-convert "26.1")
518 518
519;; Should today-only be removed in favor of timeclock-relative? - gm 519;; Should today-only be removed in favor of timeclock-relative? - gm
520(defsubst timeclock-when-to-leave (&optional today-only) 520(defsubst timeclock-when-to-leave (&optional today-only)
diff --git a/lisp/cedet/semantic/decorate/include.el b/lisp/cedet/semantic/decorate/include.el
index c91b04fb6a1..021c2d8fee9 100644
--- a/lisp/cedet/semantic/decorate/include.el
+++ b/lisp/cedet/semantic/decorate/include.el
@@ -352,9 +352,9 @@ Argument EVENT is the mouse clicked event."
352 (file (semantic-dependency-tag-file tag)) 352 (file (semantic-dependency-tag-file tag))
353 (table (when file 353 (table (when file
354 (semanticdb-file-table-object file t)))) 354 (semanticdb-file-table-object file t))))
355 (with-output-to-temp-buffer (help-buffer) ; "*Help*" 355 (help-setup-xref (list #'semantic-decoration-include-describe)
356 (help-setup-xref (list #'semantic-decoration-include-describe) 356 (called-interactively-p 'interactive))
357 (called-interactively-p 'interactive)) 357 (with-help-window (help-buffer) ; "*Help*"
358 (princ "Include File: ") 358 (princ "Include File: ")
359 (princ (semantic-format-tag-name tag nil t)) 359 (princ (semantic-format-tag-name tag nil t))
360 (princ "\n") 360 (princ "\n")
@@ -451,9 +451,9 @@ Argument EVENT is the mouse clicked event."
451 (interactive) 451 (interactive)
452 (let ((tag (semantic-current-tag)) 452 (let ((tag (semantic-current-tag))
453 (mm major-mode)) 453 (mm major-mode))
454 (with-output-to-temp-buffer (help-buffer) ; "*Help*" 454 (help-setup-xref (list #'semantic-decoration-unknown-include-describe)
455 (help-setup-xref (list #'semantic-decoration-unknown-include-describe) 455 (called-interactively-p 'interactive))
456 (called-interactively-p 'interactive)) 456 (with-help-window (help-buffer) ; "*Help*"
457 (princ "Include File: ") 457 (princ "Include File: ")
458 (princ (semantic-format-tag-name tag nil t)) 458 (princ (semantic-format-tag-name tag nil t))
459 (princ "\n\n") 459 (princ "\n\n")
@@ -534,9 +534,9 @@ Argument EVENT is the mouse clicked event."
534 (let* ((tag (semantic-current-tag)) 534 (let* ((tag (semantic-current-tag))
535 (table (semanticdb-find-table-for-include tag (current-buffer))) 535 (table (semanticdb-find-table-for-include tag (current-buffer)))
536 ) ;; (mm major-mode) 536 ) ;; (mm major-mode)
537 (with-output-to-temp-buffer (help-buffer) ; "*Help*" 537 (help-setup-xref (list #'semantic-decoration-fileless-include-describe)
538 (help-setup-xref (list #'semantic-decoration-fileless-include-describe) 538 (called-interactively-p 'interactive))
539 (called-interactively-p 'interactive)) 539 (with-help-window (help-buffer) ; "*Help*"
540 (princ "Include Tag: ") 540 (princ "Include Tag: ")
541 (princ (semantic-format-tag-name tag nil t)) 541 (princ (semantic-format-tag-name tag nil t))
542 (princ "\n\n") 542 (princ "\n\n")
@@ -573,10 +573,9 @@ Argument EVENT describes the event that caused this function to be called."
573Argument EVENT is the mouse clicked event." 573Argument EVENT is the mouse clicked event."
574 (interactive) 574 (interactive)
575 (let ((tag (semantic-current-tag))) 575 (let ((tag (semantic-current-tag)))
576 (with-output-to-temp-buffer (help-buffer); "*Help*" 576 (help-setup-xref (list #'semantic-decoration-unparsed-include-describe)
577 (help-setup-xref (list #'semantic-decoration-unparsed-include-describe) 577 (called-interactively-p 'interactive))
578 (called-interactively-p 'interactive)) 578 (with-help-window (help-buffer); "*Help*"
579
580 (princ "Include File: ") 579 (princ "Include File: ")
581 (princ (semantic-format-tag-name tag nil t)) 580 (princ (semantic-format-tag-name tag nil t))
582 (princ "\n") 581 (princ "\n")
@@ -654,10 +653,9 @@ Argument EVENT describes the event that caused this function to be called."
654 (tags (semantic-fetch-tags)) 653 (tags (semantic-fetch-tags))
655 (inc (semantic-find-tags-by-class 'include table)) 654 (inc (semantic-find-tags-by-class 'include table))
656 ) 655 )
657 (with-output-to-temp-buffer (help-buffer) ;"*Help*" 656 (help-setup-xref (list #'semantic-decoration-all-include-summary)
658 (help-setup-xref (list #'semantic-decoration-all-include-summary) 657 (called-interactively-p 'interactive))
659 (called-interactively-p 'interactive)) 658 (with-help-window (help-buffer) ;"*Help*"
660
661 (princ "Include Summary for File: ") 659 (princ "Include Summary for File: ")
662 (princ (file-truename (buffer-file-name))) 660 (princ (file-truename (buffer-file-name)))
663 (princ "\n") 661 (princ "\n")
diff --git a/lisp/cedet/semantic/util.el b/lisp/cedet/semantic/util.el
index a1cd2cfde24..2ca8de839b5 100644
--- a/lisp/cedet/semantic/util.el
+++ b/lisp/cedet/semantic/util.el
@@ -271,10 +271,9 @@ If TAG is not specified, use the tag at point."
271 (interactive) 271 (interactive)
272 (let ((buff (current-buffer)) 272 (let ((buff (current-buffer))
273 ) 273 )
274 274 (help-setup-xref (list #'semantic-describe-buffer)
275 (with-output-to-temp-buffer (help-buffer) 275 (called-interactively-p 'interactive))
276 (help-setup-xref (list #'semantic-describe-buffer) 276 (with-help-window (help-buffer)
277 (called-interactively-p 'interactive))
278 (with-current-buffer standard-output 277 (with-current-buffer standard-output
279 (princ "Semantic Configuration in ") 278 (princ "Semantic Configuration in ")
280 (princ (buffer-name buff)) 279 (princ (buffer-name buff))
diff --git a/lisp/comint.el b/lisp/comint.el
index f4d484f037d..8d2692e50e6 100644
--- a/lisp/comint.el
+++ b/lisp/comint.el
@@ -3715,6 +3715,9 @@ last function is the text that is actually inserted in the redirection buffer.
3715You can use `add-hook' to add functions to this list 3715You can use `add-hook' to add functions to this list
3716either globally or locally.") 3716either globally or locally.")
3717 3717
3718(defvar comint-redirect-hook nil
3719 "Normal hook run after completing a comint-redirect.")
3720
3718;; Internal variables 3721;; Internal variables
3719 3722
3720(defvar comint-redirect-output-buffer nil 3723(defvar comint-redirect-output-buffer nil
diff --git a/lisp/desktop.el b/lisp/desktop.el
index 74961032303..df98079b1c2 100644
--- a/lisp/desktop.el
+++ b/lisp/desktop.el
@@ -1064,13 +1064,19 @@ DIRNAME must be the directory in which the desktop file will be saved."
1064 1064
1065;; ---------------------------------------------------------------------------- 1065;; ----------------------------------------------------------------------------
1066(defun desktop--check-dont-save (frame) 1066(defun desktop--check-dont-save (frame)
1067 (not (frame-parameter frame 'desktop-dont-save))) 1067 (and (not (frame-parameter frame 'desktop-dont-save))
1068 ;; Don't save daemon initial frames, since we cannot (and don't
1069 ;; need to) restore them.
1070 (not (and (daemonp)
1071 (equal (terminal-name (frame-terminal frame))
1072 "initial_terminal")))))
1068 1073
1069(defconst desktop--app-id `(desktop . ,desktop-file-version)) 1074(defconst desktop--app-id `(desktop . ,desktop-file-version))
1070 1075
1071(defun desktop-save-frameset () 1076(defun desktop-save-frameset ()
1072 "Save the state of existing frames in `desktop-saved-frameset'. 1077 "Save the state of existing frames in `desktop-saved-frameset'.
1073Frames with a non-nil `desktop-dont-save' parameter are not saved." 1078Frames with a non-nil `desktop-dont-save' parameter are not saved.
1079Likewise the initial frame of a daemon sesion."
1074 (setq desktop-saved-frameset 1080 (setq desktop-saved-frameset
1075 (and desktop-restore-frames 1081 (and desktop-restore-frames
1076 (frameset-save nil 1082 (frameset-save nil
diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index 30311e1a8ed..4e28dd400ce 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -955,7 +955,7 @@ It's a subdirectory of `doc-view-cache-directory'."
955(defun doc-view-mode-p (type) 955(defun doc-view-mode-p (type)
956 "Return non-nil if document type TYPE is available for `doc-view'. 956 "Return non-nil if document type TYPE is available for `doc-view'.
957Document types are symbols like `dvi', `ps', `pdf', `epub', 957Document types are symbols like `dvi', `ps', `pdf', `epub',
958`cbz', `fb2', `xps', `oxps', or`odf' (any OpenDocument format)." 958`cbz', `fb2', `xps', `oxps', or `odf' (any OpenDocument format)."
959 (and (display-graphic-p) 959 (and (display-graphic-p)
960 (image-type-available-p 'png) 960 (image-type-available-p 'png)
961 (cond 961 (cond
diff --git a/lisp/emacs-lisp/byte-opt.el b/lisp/emacs-lisp/byte-opt.el
index c5458c1ba69..0290a2fd6ca 100644
--- a/lisp/emacs-lisp/byte-opt.el
+++ b/lisp/emacs-lisp/byte-opt.el
@@ -341,14 +341,13 @@ There can be multiple entries for the same NAME if it has several aliases.")
341 (if (cdr exps) 341 (if (cdr exps)
342 (macroexp-progn (byte-optimize-body exps for-effect)) 342 (macroexp-progn (byte-optimize-body exps for-effect))
343 (byte-optimize-form (car exps) for-effect))) 343 (byte-optimize-form (car exps) for-effect)))
344
344 (`(prog1 ,exp . ,exps) 345 (`(prog1 ,exp . ,exps)
345 (let ((exp-opt (byte-optimize-form exp for-effect))) 346 (let ((exp-opt (byte-optimize-form exp for-effect))
346 (if exps 347 (exps-opt (byte-optimize-body exps t)))
347 (let ((exps-opt (byte-optimize-body exps t))) 348 (cond ((null exps-opt) exp-opt)
348 (if (macroexp-const-p exp-opt) 349 ((macroexp-const-p exp-opt) `(progn ,@exps-opt ,exp-opt))
349 `(progn ,@exps-opt ,exp-opt) 350 (t `(,fn ,exp-opt ,@exps-opt)))))
350 `(,fn ,exp-opt ,@exps-opt)))
351 exp-opt)))
352 351
353 (`(,(or `save-excursion `save-restriction `save-current-buffer) . ,exps) 352 (`(,(or `save-excursion `save-restriction `save-current-buffer) . ,exps)
354 ;; Those subrs which have an implicit progn; it's not quite good 353 ;; Those subrs which have an implicit progn; it's not quite good
diff --git a/lisp/emacs-lisp/bytecomp.el b/lisp/emacs-lisp/bytecomp.el
index fbb2b8e6971..98dbbb8e2f8 100644
--- a/lisp/emacs-lisp/bytecomp.el
+++ b/lisp/emacs-lisp/bytecomp.el
@@ -1857,7 +1857,8 @@ It is too wide if it has any lines longer than the largest of
1857 ;; The native compiler doesn't use those dynamic docstrings. 1857 ;; The native compiler doesn't use those dynamic docstrings.
1858 (not byte-native-compiling) 1858 (not byte-native-compiling)
1859 ;; Docstrings can only be dynamic when compiling a file. 1859 ;; Docstrings can only be dynamic when compiling a file.
1860 byte-compile--\#$) 1860 byte-compile--\#$
1861 (not (equal doc ""))) ; empty lazy strings are pointless
1861 (let* ((byte-pos (with-memoization 1862 (let* ((byte-pos (with-memoization
1862 ;; Reuse a previously written identical docstring. 1863 ;; Reuse a previously written identical docstring.
1863 ;; This is not done out of thriftiness but to try and 1864 ;; This is not done out of thriftiness but to try and
@@ -5142,7 +5143,8 @@ binding slots have been popped."
5142 (when (stringp doc) 5143 (when (stringp doc)
5143 (setq rest (byte-compile--list-with-n 5144 (setq rest (byte-compile--list-with-n
5144 rest 0 5145 rest 0
5145 (byte-compile--docstring doc (nth 0 form) name))))) 5146 (byte-compile--docstring doc (nth 0 form) name)))
5147 (setq form (nconc (take 3 form) rest))))
5146 (pcase-let* 5148 (pcase-let*
5147 ;; `macro' is non-nil if it defines a macro. 5149 ;; `macro' is non-nil if it defines a macro.
5148 ;; `fun' is the function part of `arg' (defaults to `arg'). 5150 ;; `fun' is the function part of `arg' (defaults to `arg').
diff --git a/lisp/emacs-lisp/checkdoc.el b/lisp/emacs-lisp/checkdoc.el
index c9f9082a27a..fd226b89fda 100644
--- a/lisp/emacs-lisp/checkdoc.el
+++ b/lisp/emacs-lisp/checkdoc.el
@@ -381,6 +381,9 @@ large number of libraries means it is impractical to fix all
381of these warnings masse. In almost any other case, setting 381of these warnings masse. In almost any other case, setting
382this to anything but t is likely to be counter-productive.") 382this to anything but t is likely to be counter-productive.")
383 383
384(defvar checkdoc--batch-flag nil
385 "Non-nil in batch mode.")
386
384(defun checkdoc-list-of-strings-p (obj) 387(defun checkdoc-list-of-strings-p (obj)
385 "Return t when OBJ is a list of strings." 388 "Return t when OBJ is a list of strings."
386 (declare (obsolete list-of-strings-p "29.1")) 389 (declare (obsolete list-of-strings-p "29.1"))
@@ -1063,12 +1066,13 @@ Optional argument INTERACT permits more interactive fixing."
1063 (e (checkdoc-rogue-space-check-engine nil nil interact)) 1066 (e (checkdoc-rogue-space-check-engine nil nil interact))
1064 (checkdoc-generate-compile-warnings-flag 1067 (checkdoc-generate-compile-warnings-flag
1065 (or take-notes checkdoc-generate-compile-warnings-flag))) 1068 (or take-notes checkdoc-generate-compile-warnings-flag)))
1066 (if (not (called-interactively-p 'interactive)) 1069 (if (not (or (called-interactively-p 'interactive) checkdoc--batch-flag))
1067 e 1070 e
1068 (if e 1071 (if e
1069 (message "%s" (checkdoc-error-text e)) 1072 (message "%s" (checkdoc-error-text e))
1070 (checkdoc-show-diagnostics) 1073 (checkdoc-show-diagnostics)
1071 (message "Space Check: done."))))) 1074 (if (called-interactively-p 'interactive)
1075 (message "Space Check: done."))))))
1072 1076
1073;;;###autoload 1077;;;###autoload
1074(defun checkdoc-message-text (&optional take-notes) 1078(defun checkdoc-message-text (&optional take-notes)
@@ -1081,7 +1085,7 @@ Optional argument TAKE-NOTES causes all errors to be logged."
1081 (checkdoc-generate-compile-warnings-flag 1085 (checkdoc-generate-compile-warnings-flag
1082 (or take-notes checkdoc-generate-compile-warnings-flag))) 1086 (or take-notes checkdoc-generate-compile-warnings-flag)))
1083 (setq e (checkdoc-message-text-search)) 1087 (setq e (checkdoc-message-text-search))
1084 (if (not (called-interactively-p 'interactive)) 1088 (if (not (or (called-interactively-p 'interactive) checkdoc--batch-flag))
1085 e 1089 e
1086 (if e 1090 (if e
1087 (user-error "%s" (checkdoc-error-text e)) 1091 (user-error "%s" (checkdoc-error-text e))
@@ -2819,7 +2823,7 @@ function called to create the messages."
2819 2823
2820(defun checkdoc-show-diagnostics () 2824(defun checkdoc-show-diagnostics ()
2821 "Display the checkdoc diagnostic buffer in a temporary window." 2825 "Display the checkdoc diagnostic buffer in a temporary window."
2822 (if checkdoc-pending-errors 2826 (if (and checkdoc-pending-errors (not checkdoc--batch-flag))
2823 (let* ((b (get-buffer checkdoc-diagnostic-buffer)) 2827 (let* ((b (get-buffer checkdoc-diagnostic-buffer))
2824 (win (if b (display-buffer b)))) 2828 (win (if b (display-buffer b))))
2825 (when win 2829 (when win
@@ -2832,6 +2836,23 @@ function called to create the messages."
2832 (setq checkdoc-pending-errors nil) 2836 (setq checkdoc-pending-errors nil)
2833 nil))) 2837 nil)))
2834 2838
2839
2840;;;###autoload
2841(defun checkdoc-batch ()
2842 "Check current buffer in batch mode.
2843Report any errors and signal the first found error."
2844 (when noninteractive
2845 (let ((checkdoc-autofix-flag nil)
2846 (checkdoc--batch-flag t))
2847 (checkdoc-current-buffer t)
2848 (when checkdoc-pending-errors
2849 (when-let* ((b (get-buffer checkdoc-diagnostic-buffer)))
2850 (with-current-buffer b
2851 (princ (buffer-string)))
2852 (terpri))
2853 (checkdoc-current-buffer)))))
2854
2855
2835(defun checkdoc-get-keywords () 2856(defun checkdoc-get-keywords ()
2836 "Return a list of package keywords for the current file." 2857 "Return a list of package keywords for the current file."
2837 (save-excursion 2858 (save-excursion
diff --git a/lisp/emacs-lisp/cond-star.el b/lisp/emacs-lisp/cond-star.el
index fc8e261339e..33a21602d9b 100644
--- a/lisp/emacs-lisp/cond-star.el
+++ b/lisp/emacs-lisp/cond-star.el
@@ -58,7 +58,7 @@ normally has the form (CONDITION BODY...).
58 58
59CONDITION can be a Lisp expression, as in `cond'. 59CONDITION can be a Lisp expression, as in `cond'.
60Or it can be one of `(bind* BINDINGS...)', `(match* PATTERN DATUM)', 60Or it can be one of `(bind* BINDINGS...)', `(match* PATTERN DATUM)',
61or `(pcase* PATTERN DATUM)', 61`(bind-and* BINDINGS...)' or `(pcase* PATTERN DATUM)',
62 62
63`(bind* BINDINGS...)' means to bind BINDINGS (as if they were in `let*') 63`(bind* BINDINGS...)' means to bind BINDINGS (as if they were in `let*')
64for the body of the clause, and all subsequent clauses, since the `bind*' 64for the body of the clause, and all subsequent clauses, since the `bind*'
@@ -81,15 +81,17 @@ When a clause's condition is true, and it exits the `cond*'
81or is the last clause, the value of the last expression 81or is the last clause, the value of the last expression
82in its body becomes the return value of the `cond*' construct. 82in its body becomes the return value of the `cond*' construct.
83 83
84Non-exit clause: 84Non-exit clauses:
85 85
86If a clause has only one element, or if its first element is 86If a clause has only one element, or if its first element is t or a
87t or a `bind*' clause, this clause never exits the `cond*' construct. 87`bind*' form, or if it ends with the keyword `:non-exit', then this
88Instead, control always falls through to the next clause (if any). 88clause never exits the `cond*' construct. Instead, control always falls
89All bindings made in CONDITION for the BODY of the non-exit clause 89through to the next clause (if any). Except for a `bind-and*' clause,
90are passed along to the rest of the clauses in this `cond*' construct. 90all bindings made in CONDITION for the BODY of the non-exit clause are
91passed along to the rest of the clauses in this `cond*' construct.
91 92
92\\[match*] for documentation of the patterns for use in `match*'." 93See `match*' for documentation of the patterns for use in `match*'
94conditions."
93 ;; FIXME: Want an Edebug declaration. 95 ;; FIXME: Want an Edebug declaration.
94 (cond*-convert clauses)) 96 (cond*-convert clauses))
95 97
@@ -195,7 +197,9 @@ CONDITION of a `cond*' clause. See `cond*' for details."
195 (or (eq (car clause) t) 197 (or (eq (car clause) t)
196 ;; Starts with a `bind*' pseudo-form. 198 ;; Starts with a `bind*' pseudo-form.
197 (and (consp (car clause)) 199 (and (consp (car clause))
198 (eq (caar clause) 'bind*)))))) 200 (eq (caar clause) 'bind*))))
201 ;; Ends with keyword.
202 (eq (car (last clause)) :non-exit)))
199 203
200(defun cond*-non-exit-clause-substance (clause) 204(defun cond*-non-exit-clause-substance (clause)
201 "For a non-exit cond* clause CLAUSE, return its substance. 205 "For a non-exit cond* clause CLAUSE, return its substance.
@@ -214,7 +218,7 @@ This removes a final keyword if that's what makes CLAUSE non-exit."
214 (cons t (cdr clause))) 218 (cons t (cdr clause)))
215 219
216 ;; Ends with keyword. 220 ;; Ends with keyword.
217 ((keywordp (car (last clause))) 221 ((eq (car (last clause)) :non-exit)
218 ;; Do NOT include the final keyword. 222 ;; Do NOT include the final keyword.
219 (butlast clause)))) 223 (butlast clause))))
220 224
diff --git a/lisp/emacs-lisp/crm.el b/lisp/emacs-lisp/crm.el
index 6bd763d2ea2..a1c7175fc66 100644
--- a/lisp/emacs-lisp/crm.el
+++ b/lisp/emacs-lisp/crm.el
@@ -255,14 +255,11 @@ with empty strings removed."
255 crm-local-must-match-map 255 crm-local-must-match-map
256 crm-local-completion-map)) 256 crm-local-completion-map))
257 (map (minibuffer-visible-completions--maybe-compose-map map)) 257 (map (minibuffer-visible-completions--maybe-compose-map map))
258 (buffer (current-buffer))
259 input) 258 input)
260 (minibuffer-with-setup-hook 259 (minibuffer-with-setup-hook
261 (lambda () 260 (lambda ()
262 (add-hook 'choose-completion-string-functions 261 (add-hook 'choose-completion-string-functions
263 'crm--choose-completion-string nil 'local) 262 'crm--choose-completion-string nil 'local)
264 (setq-local minibuffer-completion-table #'crm--collection-fn)
265 (setq-local minibuffer-completion-predicate predicate)
266 (setq-local completion-list-insert-choice-function 263 (setq-local completion-list-insert-choice-function
267 (lambda (_start _end choice) 264 (lambda (_start _end choice)
268 (let* ((beg (save-excursion 265 (let* ((beg (save-excursion
@@ -276,14 +273,9 @@ with empty strings removed."
276 (1- (point)) 273 (1- (point))
277 (point-max))))) 274 (point-max)))))
278 (completion--replace beg end choice)))) 275 (completion--replace beg end choice))))
279 ;; see completing_read in src/minibuf.c
280 (setq-local minibuffer-completion-confirm
281 (unless (eq require-match t) require-match))
282 (setq-local minibuffer--require-match require-match)
283 (setq-local minibuffer--original-buffer buffer)
284 (setq-local crm-completion-table table) 276 (setq-local crm-completion-table table)
285 (completions--start-eager-display)) 277 (use-local-map map))
286 (setq input (read-from-minibuffer 278 (setq input (completing-read
287 (format-spec 279 (format-spec
288 crm-prompt 280 crm-prompt
289 (let* ((sep (or (get-text-property 0 'separator crm-separator) 281 (let* ((sep (or (get-text-property 0 'separator crm-separator)
@@ -291,11 +283,8 @@ with empty strings removed."
291 (desc (or (get-text-property 0 'description crm-separator) 283 (desc (or (get-text-property 0 'description crm-separator)
292 (concat "list separated by " sep)))) 284 (concat "list separated by " sep))))
293 `((?s . ,sep) (?d . ,desc) (?p . ,prompt)))) 285 `((?s . ,sep) (?d . ,desc) (?p . ,prompt))))
294 initial-input map nil hist def inherit-input-method))) 286 #'crm--collection-fn predicate
295 ;; If the user enters empty input, `read-from-minibuffer' 287 require-match initial-input hist def inherit-input-method)))
296 ;; returns the empty string, not DEF.
297 (when (and def (string-equal input ""))
298 (setq input (if (consp def) (car def) def)))
299 ;; Remove empty strings in the list of read strings. 288 ;; Remove empty strings in the list of read strings.
300 (split-string input crm-separator t))) 289 (split-string input crm-separator t)))
301 290
diff --git a/lisp/emacs-lisp/loaddefs-gen.el b/lisp/emacs-lisp/loaddefs-gen.el
index 60d250b564f..0dc0d873bcd 100644
--- a/lisp/emacs-lisp/loaddefs-gen.el
+++ b/lisp/emacs-lisp/loaddefs-gen.el
@@ -732,11 +732,8 @@ instead of just updating them with the new/changed autoloads."
732 '(t (escape-newlines . t) 732 '(t (escape-newlines . t)
733 (escape-control-characters . t))) 733 (escape-control-characters . t)))
734 (insert " ")) 734 (insert " "))
735 (let ((start (point))) 735 (delete-char -1) (insert "\n")
736 (prin1 (pop def) (current-buffer) t) 736 (prin1 (pop def) (current-buffer) t)
737 (save-excursion
738 (goto-char (1+ start))
739 (insert "\\\n")))
740 (while def 737 (while def
741 (insert " ") 738 (insert " ")
742 (prin1 (pop def) (current-buffer) 739 (prin1 (pop def) (current-buffer)
diff --git a/lisp/emacs-lisp/package-activate.el b/lisp/emacs-lisp/package-activate.el
index e130304be5c..1689d985c28 100644
--- a/lisp/emacs-lisp/package-activate.el
+++ b/lisp/emacs-lisp/package-activate.el
@@ -30,6 +30,8 @@
30;; activate packages at startup, as well as other functions that are 30;; activate packages at startup, as well as other functions that are
31;; useful without having to load the entirety of package.el. 31;; useful without having to load the entirety of package.el.
32 32
33;; Note that the contents of this file are preloaded!
34
33;;; Code: 35;;; Code:
34 36
35(eval-when-compile (require 'cl-lib)) 37(eval-when-compile (require 'cl-lib))
@@ -534,5 +536,148 @@ the `Version:' header."
534 (require 'lisp-mnt) 536 (require 'lisp-mnt)
535 (lm-package-version mainfile))))))) 537 (lm-package-version mainfile)))))))
536 538
539
540;;;; Package suggestions system
541
542;; Note that only the definitions necessary to recognise package
543;; suggestions are defined here. The user interface to select and act
544;; on package suggestions is to be found in package.el.
545
546(defcustom package-autosuggest-style 'mode-line
547 "How to draw attention to `package-autosuggest-mode' suggestions.
548You can set this value to `mode-line' (default) to indicate the
549availability of a package suggestion in the minor mode, `always' to
550prompt the user in the minibuffer every time a suggestion is available
551in a `fundamental-mode' buffer, or `message' to just display a message
552hinting at the existence of a suggestion. If you only wish to be
553reminded of package suggestions once every session, consider customizing
554the `package-autosuggest-once' user option."
555 :type '(choice (const :tag "Indicate in mode line" mode-line)
556 (const :tag "Always prompt" always)
557 (const :tag "Indicate with message" message))
558 :group 'package)
559
560(defcustom package-autosuggest-once nil
561 "Non-nil means not to repeat package suggestions."
562 :type 'boolean
563 :group 'package)
564
565(defvar package--autosuggest-database 'unset
566 "A list of package suggestions.
567Each entry in the list is of a form suitable to for
568`package--suggestion-applies-p', which see. The special value `unset'
569is used to indicate that `package--autosuggest-find-candidates' should
570load the database into memory.")
571
572(defvar package--autosuggest-suggested '()
573 "List of packages that have already been suggested.
574Suggestions found in this list will not count as suggestions (e.g. if
575`package-autosuggest-style' is set to `mode-line', a suggestion found in
576here will inhibit `package-autosuggest-mode' from displaying a hint in
577the mode line).")
578
579(defun package--suggestion-applies-p (sug)
580 "Check if a suggestion SUG is applicable to the current buffer.
581Each suggestion has the form (PACKAGE TYPE DATA), where PACKAGE is a
582symbol denoting the package and major-mode the suggestion applies to,
583TYPE is one of `auto-mode-alist', `magic-mode-alist' or
584`interpreter-mode-alist' indicating the type of check to be made and
585DATA is the value to check against TYPE in the intuitive way (e.g. for
586`auto-mode-alist' DATA is a regular expression matching a file name that
587PACKAGE should be suggested for). If the package name and the major
588mode name differ, then an optional forth element MAJOR-MODE can indicate
589what command to invoke to enable the package."
590 (pcase sug
591 ((or (guard (not (eq major-mode 'fundamental-mode)))
592 (guard (and package-autosuggest-once
593 (not (memq (car sug) package--autosuggest-suggested))))
594 `(,(pred package-installed-p) . ,_))
595 nil)
596 (`(,_ auto-mode-alist ,ext . ,_)
597 (and (buffer-file-name) (string-match-p ext (buffer-file-name)) t))
598 (`(,_ magic-mode-alist ,mag . ,_)
599 (without-restriction
600 (save-excursion
601 (goto-char (point-min))
602 (looking-at-p mag))))
603 (`(,_ interpreter-mode-alist ,intr . ,_)
604 (without-restriction
605 (save-excursion
606 (goto-char (point-min))
607 (and (looking-at auto-mode-interpreter-regexp)
608 (string-match-p
609 (concat "\\`" (file-name-nondirectory (match-string 2)) "\\'")
610 intr)))))))
611
612(defun package--autosuggest-find-candidates ()
613 "Return a list of suggestions that might be interesting the current buffer.
614The elements of the returned list will have the form described in
615`package--suggestion-applies-p'."
616 (and (eq major-mode 'fundamental-mode)
617 (let ((suggetions '()))
618 (when (eq package--autosuggest-database 'unset)
619 (setq package--autosuggest-database
620 (with-temp-buffer
621 (insert-file-contents
622 (expand-file-name "package-autosuggest.eld"
623 data-directory))
624 (read (current-buffer)))))
625 (dolist (sug package--autosuggest-database)
626 (when (package--suggestion-applies-p sug)
627 (push sug suggetions)))
628 suggetions)))
629
630(defvar package--autosugest-line-format
631 '(:eval (package--autosugest-line-format)))
632(put 'package--autosugest-line-format 'risky-local-variable t)
633
634(defun package--autosugest-line-format ()
635 "Generate a mode-line string to indicate a suggested package."
636 `(,@(and-let* (((not (null package-autosuggest-mode)))
637 ((eq package-autosuggest-style 'mode-line))
638 (avail (package--autosuggest-find-candidates)))
639 (propertize
640 "[Upgrade?]"
641 'face 'mode-line-emphasis
642 'mouse-face 'mode-line-highlight
643 'help-echo "Click to install suggested package."
644 'keymap (let ((map (make-sparse-keymap)))
645 (define-key map [mode-line down-mouse-1] #'package-autosuggest)
646 map)))))
647
648(declare-function package-autosuggest "package" (&optional candidates))
649
650(defun package--autosuggest-after-change-mode ()
651 "Display package suggestions for the current buffer.
652This function should be added to `after-change-major-mode-hook'."
653 (when-let* ((avail (package--autosuggest-find-candidates))
654 (pkgs (mapconcat #'symbol-name
655 (delete-dups (mapcar #'car avail))
656 ", ")))
657 (pcase-exhaustive package-autosuggest-style
658 ('mode-line
659 (setq mode-name (append (ensure-list mode-name)
660 '((package-autosuggest-mode
661 package--autosugest-line-format))))
662 (force-mode-line-update t))
663 ('always
664 (package-autosuggest avail))
665 ('message
666 (message
667 (substitute-command-keys
668 (format "Found suggested packages: %s. Install using \\[package-autosuggest]"
669 pkgs)))
670 (dolist (rec avail)
671 (add-to-list 'package--autosuggest-suggested (car rec)))))))
672
673;;;###autoload
674(define-minor-mode package-autosuggest-mode
675 "Enable the automatic suggestion and installation of packages."
676 :global t :group 'package
677 ;; :initialize #'custom-initialize-delay
678 (funcall (if package-autosuggest-mode #'add-hook #'remove-hook)
679 'after-change-major-mode-hook
680 #'package--autosuggest-after-change-mode))
681
537(provide 'package-activate) 682(provide 'package-activate)
538;;; package-activate.el ends here 683;;; package-activate.el ends here
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el
index 407c4496d81..e2d35f20eb5 100644
--- a/lisp/emacs-lisp/package.el
+++ b/lisp/emacs-lisp/package.el
@@ -4530,6 +4530,122 @@ The list is displayed in a buffer named `*Packages*'."
4530 (list-packages t)) 4530 (list-packages t))
4531 4531
4532 4532
4533;;;; Package Suggestions
4534
4535(defun package--autosuggest-install-and-enable (sug)
4536 "Install and enable a package suggestion PKG-ENT.
4537SUG should be of the form as described in `package--suggestion-applies-p'."
4538 (let ((buffers-to-update '()))
4539 (dolist (buf (buffer-list))
4540 (with-current-buffer buf
4541 (when (and (eq major-mode 'fundamental-mode) (buffer-file-name)
4542 (package--suggestion-applies-p sug))
4543 (push buf buffers-to-update))))
4544 (with-demoted-errors "Failed to install package: %S"
4545 (package-install (car sug))
4546 (dolist (buf buffers-to-update)
4547 (with-demoted-errors "Failed to enable major mode: %S"
4548 (with-current-buffer buf
4549 (funcall-interactively (or (cadddr sug) (car sug)))))))))
4550
4551(defun package--autosugest-prompt (packages)
4552 "Query the user whether to install PACKAGES or not.
4553PACKAGES is a list of package suggestions in the form as described in
4554`package--suggestion-applies-p'. The function returns a non-nil value
4555if affirmative, otherwise nil"
4556 (let* ((inhibit-read-only t) (use-hard-newlines t)
4557 (nl (propertize "\n" 'hard t)) (nlnl (concat nl nl))
4558 (buf (current-buffer)))
4559 (with-current-buffer (get-buffer-create
4560 (format "*package suggestion: %s*"
4561 (buffer-name buf)))
4562 (erase-buffer)
4563 (insert
4564 "The buffer \""
4565 (buffer-name buf)
4566 "\" currently lacks any language-specific support.
4567The package manager can provide the editor support for these kinds of
4568files by downloading a package from Emacs's package archive:" nl)
4569
4570 (when (length> packages 1)
4571 (insert nl "(Note that there are multiple candidate packages,
4572so you have to select which to install!)" nl))
4573
4574 (pcase-dolist (`(,pkg . ,sugs) (seq-group-by #'car packages))
4575 (insert nl "* "
4576 (buttonize (concat "Install " (symbol-name pkg))
4577 (lambda (_)
4578 (package--autosuggest-install-and-enable
4579 (car sugs))
4580 (quit-window)))
4581 " ("
4582 (buttonize "about"
4583 (lambda (_)
4584 (unless (assq pkg package-archive-contents)
4585 (package-read-all-archive-contents))
4586 (describe-package pkg)))
4587 ", matches ")
4588 (dolist (sug sugs)
4589 (unless (eq (char-before) ?\s)
4590 (insert ", "))
4591 (pcase sug
4592 (`(,_ auto-mode-alist . ,_)
4593 (insert "file extension "))
4594 (`(,_ magic-mode-alist . ,_)
4595 (insert "magic bytes"))
4596 (`(,_ interpreter-mode-alist . ,_)
4597 (insert "interpreter "))))
4598 (delete-horizontal-space) (insert ").")
4599
4600 (add-to-list 'package--autosuggest-suggested pkg))
4601
4602 (insert nl "* " (buttonize "Do not install anything" (lambda (_) (quit-window))) "."
4603 nl "* " (buttonize "Permanently disable package suggestions"
4604 (lambda (_)
4605 (customize-save-variable
4606 'package-autosuggest-mode nil
4607 "Disabled at user's request")
4608 (quit-window)))
4609 "."
4610
4611 nlnl "To learn more about package management, read "
4612 (buttonize "(emacs) Packages" (lambda (_) (info "(emacs) Packages")))
4613 ", and to learn more about how Emacs supports specific languages, read "
4614 (buttonize "(emacs) Major modes" (lambda (_) (info "(emacs) Major modes")))
4615 ".")
4616
4617 (fill-region (point-min) (point-max))
4618 (special-mode)
4619 (button-mode t)
4620
4621 (let ((win (display-buffer-below-selected (current-buffer) '())))
4622 (fit-window-to-buffer win)
4623 (select-window win)
4624 (set-window-dedicated-p win t)
4625 (set-window-point win (point-min))))))
4626
4627;;;###autoload
4628(defun package-autosuggest (&optional candidates)
4629 "Prompt the user to install the suggested packages.
4630The optional argument CANDIDATES may be a list of packages that match
4631for form described in `package--suggestion-applies-p'. If omitted, the
4632list of candidates will be computed from the database."
4633 (interactive)
4634 (package--autosugest-prompt
4635 (or candidates
4636 (package--autosuggest-find-candidates)
4637 (user-error "No package suggestions found"))))
4638
4639(defun package-reset-suggestions ()
4640 "Forget previous package suggestions.
4641Emacs will remember if you have previously rejected a suggestion during
4642a session and won't mention it afterwards. If you have made a mistake
4643or would like to reconsider this, use this command to want to reset the
4644suggestions."
4645 (interactive)
4646 (setq package--autosuggest-suggested nil))
4647
4648
4533;;;; Quickstart: precompute activation actions for faster start up. 4649;;;; Quickstart: precompute activation actions for faster start up.
4534 4650
4535(defvar Info-directory-list) 4651(defvar Info-directory-list)
diff --git a/lisp/emacs-lisp/seq.el b/lisp/emacs-lisp/seq.el
index 4963624ee2d..e0a41b380b5 100644
--- a/lisp/emacs-lisp/seq.el
+++ b/lisp/emacs-lisp/seq.el
@@ -567,7 +567,7 @@ This does not modify SEQUENCE1 or SEQUENCE2."
567 567
568;;;###autoload 568;;;###autoload
569(cl-defgeneric seq-intersection (sequence1 sequence2 &optional testfn) 569(cl-defgeneric seq-intersection (sequence1 sequence2 &optional testfn)
570 "Return copy of SEQUENCE1 with elements that appear in SEQUENCE2 removed. 570 "Return copy of SEQUENCE1 with elements that do not appear in SEQUENCE2 removed.
571\"Equality\" of elements is defined by the function TESTFN, which 571\"Equality\" of elements is defined by the function TESTFN, which
572defaults to `equal'. 572defaults to `equal'.
573This does not modify SEQUENCE1 or SEQUENCE2." 573This does not modify SEQUENCE1 or SEQUENCE2."
@@ -579,7 +579,7 @@ This does not modify SEQUENCE1 or SEQUENCE2."
579 '())) 579 '()))
580 580
581(cl-defgeneric seq-difference (sequence1 sequence2 &optional testfn) 581(cl-defgeneric seq-difference (sequence1 sequence2 &optional testfn)
582 "Return list of all the elements that appear in SEQUENCE1 but not in SEQUENCE2. 582 "Return copy of SEQUENCE1 with elements that appear in SEQUENCE2 removed.
583\"Equality\" of elements is defined by the function TESTFN, which 583\"Equality\" of elements is defined by the function TESTFN, which
584defaults to `equal'. 584defaults to `equal'.
585This does not modify SEQUENCE1 or SEQUENCE2." 585This does not modify SEQUENCE1 or SEQUENCE2."
diff --git a/lisp/emacs-lisp/shortdoc.el b/lisp/emacs-lisp/shortdoc.el
index 70583e08dbd..8b382bd14dd 100644
--- a/lisp/emacs-lisp/shortdoc.el
+++ b/lisp/emacs-lisp/shortdoc.el
@@ -1707,7 +1707,9 @@ function's documentation in the Info manual"))
1707 ;; Doc string. 1707 ;; Doc string.
1708 (insert " " 1708 (insert " "
1709 (or (plist-get data :doc) 1709 (or (plist-get data :doc)
1710 (car (split-string (documentation function) "\n")))) 1710 (car (split-string (or (documentation function)
1711 "Error: missing docstring.")
1712 "\n"))))
1711 (insert "\n") 1713 (insert "\n")
1712 (add-face-text-property start-section (point) 'shortdoc-section t) 1714 (add-face-text-property start-section (point) 'shortdoc-section t)
1713 (let ((print-escape-newlines t) 1715 (let ((print-escape-newlines t)
diff --git a/lisp/emacs-lisp/smie.el b/lisp/emacs-lisp/smie.el
index 91f3332a79b..33821b8be28 100644
--- a/lisp/emacs-lisp/smie.el
+++ b/lisp/emacs-lisp/smie.el
@@ -1153,7 +1153,7 @@ METHOD can be:
1153- :before, in which case ARG is a token and the function should return the 1153- :before, in which case ARG is a token and the function should return the
1154 OFFSET to use to indent ARG itself. 1154 OFFSET to use to indent ARG itself.
1155- :elem, in which case the function should return either: 1155- :elem, in which case the function should return either:
1156 - the offset to use to indent function arguments (ARG = `arg') 1156 - the offset to use to indent function arguments (ARG = `args')
1157 - the basic indentation step (ARG = `basic'). 1157 - the basic indentation step (ARG = `basic').
1158 - the token to use (when ARG = `empty-line-token') when we don't know how 1158 - the token to use (when ARG = `empty-line-token') when we don't know how
1159 to indent an empty line. 1159 to indent an empty line.
diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index 8d04958487f..b36b14b9b50 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -37,6 +37,7 @@
37 37
38(eval-when-compile (require 'cl-lib)) 38(eval-when-compile (require 'cl-lib))
39 39
40(require 'mule-util)
40 41
41(defmacro internal--thread-argument (first? &rest forms) 42(defmacro internal--thread-argument (first? &rest forms)
42 "Internal implementation for `thread-first' and `thread-last'. 43 "Internal implementation for `thread-first' and `thread-last'.
@@ -357,6 +358,29 @@ buffer when possible, instead of creating a new one on each call."
357 (progn ,@body) 358 (progn ,@body)
358 (work-buffer--release ,work-buffer)))))) 359 (work-buffer--release ,work-buffer))))))
359 360
361(defun work-buffer--prepare-pixelwise (string buffer)
362 "Set up the current buffer to correctly compute STRING's pixel width.
363Call this with a work buffer as the current buffer.
364BUFFER is the originating buffer and if non-nil, make the current
365buffer's (work buffer) face remappings match it."
366 (when buffer
367 (dolist (v '(face-remapping-alist
368 char-property-alias-alist
369 default-text-properties))
370 (if (local-variable-p v buffer)
371 (set (make-local-variable v)
372 (buffer-local-value v buffer)))))
373 ;; Avoid deactivating the region as side effect.
374 (let (deactivate-mark)
375 (insert string))
376 ;; If `display-line-numbers' is enabled in internal
377 ;; buffers (e.g. globally), it breaks width calculation
378 ;; (bug#59311). Disable `line-prefix' and `wrap-prefix',
379 ;; for the same reason.
380 (add-text-properties
381 (point-min) (point-max)
382 '(display-line-numbers-disable t line-prefix "" wrap-prefix "")))
383
360;;;###autoload 384;;;###autoload
361(defun string-pixel-width (string &optional buffer) 385(defun string-pixel-width (string &optional buffer)
362 "Return the width of STRING in pixels. 386 "Return the width of STRING in pixels.
@@ -371,27 +395,71 @@ substring that does not include newlines."
371 ;; Keeping a work buffer around is more efficient than creating a 395 ;; Keeping a work buffer around is more efficient than creating a
372 ;; new temporary buffer. 396 ;; new temporary buffer.
373 (with-work-buffer 397 (with-work-buffer
374 ;; Setup current buffer to correctly compute pixel width. 398 (work-buffer--prepare-pixelwise string buffer)
375 (when buffer
376 (dolist (v '(face-remapping-alist
377 char-property-alias-alist
378 default-text-properties))
379 (if (local-variable-p v buffer)
380 (set (make-local-variable v)
381 (buffer-local-value v buffer)))))
382 ;; Avoid deactivating the region as side effect.
383 (let (deactivate-mark)
384 (insert string))
385 ;; If `display-line-numbers' is enabled in internal
386 ;; buffers (e.g. globally), it breaks width calculation
387 ;; (bug#59311). Disable `line-prefix' and `wrap-prefix',
388 ;; for the same reason.
389 (add-text-properties
390 (point-min) (point-max)
391 '(display-line-numbers-disable t line-prefix "" wrap-prefix ""))
392 (car (buffer-text-pixel-size nil nil t))))) 399 (car (buffer-text-pixel-size nil nil t)))))
393 400
394;;;###autoload 401;;;###autoload
402(defun truncate-string-pixelwise (string max-pixels &optional buffer
403 ellipsis ellipsis-pixels)
404 "Return STRING truncated to fit within MAX-PIXELS.
405If BUFFER is non-nil, use the face remappings, alternative and default
406properties from that buffer when determining the width.
407If you call this function to measure pixel width of a string
408with embedded newlines, it returns the width of the widest
409substring that does not include newlines.
410
411If ELLIPSIS is non-nil, it should be a string which will replace the end
412of STRING if it extends beyond MAX-PIXELS, unless the pixel width of
413STRING is equal to or less than the pixel width of ELLIPSIS. If it is
414non-nil and not a string, then ELLIPSIS defaults to
415`truncate-string-ellipsis', or to three dots when it's nil.
416
417If ELLIPSIS-PIXELS is non-nil, it is the pixel width of ELLIPSIS, and
418can be used to avoid the cost of recomputing this for multiple calls to
419this function using the same ELLIPSIS."
420 (declare (important-return-value t))
421 (if (zerop (length string))
422 string
423 ;; Keeping a work buffer around is more efficient than creating a
424 ;; new temporary buffer.
425 (let ((original-buffer (or buffer (current-buffer))))
426 (with-work-buffer
427 (work-buffer--prepare-pixelwise string buffer)
428 (set-window-buffer nil (current-buffer) 'keep-margins)
429 ;; Use a binary search to prune the number of calls to
430 ;; `window-text-pixel-size'.
431 ;; These are 1-based buffer indexes.
432 (let* ((low 1)
433 (high (1+ (length string)))
434 mid)
435 (when (> (car (window-text-pixel-size nil 1 high)) max-pixels)
436 (when (and ellipsis (not (stringp ellipsis)))
437 (setq ellipsis (truncate-string-ellipsis)))
438 (setq ellipsis-pixels (if ellipsis
439 (if ellipsis-pixels
440 ellipsis-pixels
441 (string-pixel-width ellipsis buffer))
442 0))
443 (let ((adjusted-pixels
444 (if (> max-pixels ellipsis-pixels)
445 (- max-pixels ellipsis-pixels)
446 max-pixels)))
447 (while (<= low high)
448 (setq mid (floor (+ low high) 2))
449 (if (<= (car (window-text-pixel-size nil 1 mid))
450 adjusted-pixels)
451 (setq low (1+ mid))
452 (setq high (1- mid))))))
453 (set-window-buffer nil original-buffer 'keep-margins)
454 (if mid
455 ;; Binary search ran.
456 (if (and ellipsis (> max-pixels ellipsis-pixels))
457 (concat (substring string 0 (1- high)) ellipsis)
458 (substring string 0 (1- high)))
459 ;; Fast path.
460 string))))))
461
462;;;###autoload
395(defun string-glyph-split (string) 463(defun string-glyph-split (string)
396 "Split STRING into a list of strings representing separate glyphs. 464 "Split STRING into a list of strings representing separate glyphs.
397This takes into account combining characters and grapheme clusters: 465This takes into account combining characters and grapheme clusters:
diff --git a/lisp/frameset.el b/lisp/frameset.el
index 85a90f67c68..e11a1da7e9b 100644
--- a/lisp/frameset.el
+++ b/lisp/frameset.el
@@ -1362,9 +1362,18 @@ All keyword parameters default to nil."
1362 ;; Clean up the frame list 1362 ;; Clean up the frame list
1363 (when cleanup-frames 1363 (when cleanup-frames
1364 (let ((map nil) 1364 (let ((map nil)
1365 (cleanup (if (eq cleanup-frames t) 1365 (cleanup
1366 (lambda (frame action) 1366 (if (eq cleanup-frames t)
1367 (when (memq action '(:rejected :ignored)) 1367 (lambda (frame action)
1368 (when (and (memq action '(:rejected :ignored))
1369 ;; Don't try deleting the daemon's initial
1370 ;; frame, as that would only trigger
1371 ;; warnings.
1372 (not
1373 (and (daemonp)
1374 (equal (terminal-name (frame-terminal
1375 frame))
1376 "initial_terminal"))))
1368 (delete-frame frame))) 1377 (delete-frame frame)))
1369 cleanup-frames))) 1378 cleanup-frames)))
1370 (maphash (lambda (frame _action) (push frame map)) frameset--action-map) 1379 (maphash (lambda (frame _action) (push frame map)) frameset--action-map)
diff --git a/lisp/gnus/gnus-group.el b/lisp/gnus/gnus-group.el
index 7214b440732..28c8c677d13 100644
--- a/lisp/gnus/gnus-group.el
+++ b/lisp/gnus/gnus-group.el
@@ -640,6 +640,7 @@ simple manner."
640 "M-&" #'gnus-group-universal-argument 640 "M-&" #'gnus-group-universal-argument
641 "#" #'gnus-group-mark-group 641 "#" #'gnus-group-mark-group
642 "M-#" #'gnus-group-unmark-group 642 "M-#" #'gnus-group-unmark-group
643 "M-i" #'gnus-symbolic-argument
643 644
644 "~" (define-keymap :prefix 'gnus-group-cloud-map 645 "~" (define-keymap :prefix 'gnus-group-cloud-map
645 "u" #'gnus-cloud-upload-all-data 646 "u" #'gnus-cloud-upload-all-data
diff --git a/lisp/gnus/gnus-topic.el b/lisp/gnus/gnus-topic.el
index 315f1a018c9..4fb796105e2 100644
--- a/lisp/gnus/gnus-topic.el
+++ b/lisp/gnus/gnus-topic.el
@@ -1158,7 +1158,8 @@ articles in the topic and its subtopics."
1158 #'gnus-topic-group-indentation) 1158 #'gnus-topic-group-indentation)
1159 (setq-local gnus-group-update-group-function 1159 (setq-local gnus-group-update-group-function
1160 #'gnus-topic-update-topics-containing-group) 1160 #'gnus-topic-update-topics-containing-group)
1161 (setq-local gnus-group-sort-alist-function #'gnus-group-sort-topic) 1161 (setq-local gnus-group-sort-alist-function #'gnus-group-sort-topic
1162 gnus-group-sort-selected-function #'gnus-group-sort-selected-topic)
1162 (setq gnus-group-change-level-function #'gnus-topic-change-level) 1163 (setq gnus-group-change-level-function #'gnus-topic-change-level)
1163 (setq gnus-goto-missing-group-function #'gnus-topic-goto-missing-group) 1164 (setq gnus-goto-missing-group-function #'gnus-topic-goto-missing-group)
1164 (add-hook 'gnus-check-bogus-groups-hook #'gnus-topic-clean-alist 1165 (add-hook 'gnus-check-bogus-groups-hook #'gnus-topic-clean-alist
@@ -1173,7 +1174,8 @@ articles in the topic and its subtopics."
1173 (setq gnus-group-change-level-function nil) 1174 (setq gnus-group-change-level-function nil)
1174 (remove-hook 'gnus-check-bogus-groups-hook #'gnus-topic-clean-alist) 1175 (remove-hook 'gnus-check-bogus-groups-hook #'gnus-topic-clean-alist)
1175 (setq gnus-group-prepare-function #'gnus-group-prepare-flat) 1176 (setq gnus-group-prepare-function #'gnus-group-prepare-flat)
1176 (setq gnus-group-sort-alist-function #'gnus-group-sort-flat)) 1177 (setq gnus-group-sort-alist-function #'gnus-group-sort-flat
1178 gnus-group-sort-selected-function #'gnus-group-sort-selected-flat))
1177 (when (called-interactively-p 'any) 1179 (when (called-interactively-p 'any)
1178 (gnus-group-list-groups)))) 1180 (gnus-group-list-groups))))
1179 1181
@@ -1651,6 +1653,28 @@ If performed on a topic, edit the topic parameters instead."
1651 (setcar alist (delete "dummy.group" (car alist))) 1653 (setcar alist (delete "dummy.group" (car alist)))
1652 (gnus-topic-sort-topic (pop alist) func reverse)))) 1654 (gnus-topic-sort-topic (pop alist) func reverse))))
1653 1655
1656(defun gnus-group-sort-selected-topic (groups func reverse)
1657 "Sort selected GROUPS in the topics according to FUNC and REVERSE."
1658 (let ((alist gnus-topic-alist))
1659 (while alist
1660 ;; !!!Sometimes nil elements sneak into the alist,
1661 ;; for some reason or other.
1662 (setcar alist (delq nil (car alist)))
1663 (setcar alist (delete "dummy.group" (car alist)))
1664 (let* ((topic (pop alist))
1665 (inter (seq-intersection groups (cdr topic))))
1666 ;; Do something only if there are some selected groups in this
1667 ;; topic.
1668 (when inter
1669 (let ((sorted (mapcar #'gnus-info-group
1670 (sort (mapcar #'gnus-get-info inter) func))))
1671 ;; Do the reversal, if necessary.
1672 (when reverse
1673 (setq sorted (nreverse (cdr sorted))))
1674 ;; Set the topic contents as the union of the sorted
1675 ;; selected groups and its previous contents.
1676 (setcdr topic (seq-union sorted (cdr topic)))))))))
1677
1654(defun gnus-topic-sort-topic (topic func reverse) 1678(defun gnus-topic-sort-topic (topic func reverse)
1655 ;; Each topic only lists the name of the group, while 1679 ;; Each topic only lists the name of the group, while
1656 ;; the sort predicates expect group infos as inputs. 1680 ;; the sort predicates expect group infos as inputs.
diff --git a/lisp/help-fns.el b/lisp/help-fns.el
index 6cbc75f92fb..f1ee109c87e 100644
--- a/lisp/help-fns.el
+++ b/lisp/help-fns.el
@@ -40,8 +40,8 @@
40 40
41(defvar help-fns-describe-function-functions nil 41(defvar help-fns-describe-function-functions nil
42 "List of functions to run in help buffer in `describe-function'. 42 "List of functions to run in help buffer in `describe-function'.
43Those functions will be run after the header line and argument 43Those functions will be run after the header line, the argument
44list was inserted, and before the documentation is inserted. 44list, and the function's documentation are inserted.
45The functions will be called with one argument: the function's symbol. 45The functions will be called with one argument: the function's symbol.
46They can assume that a newline was output just before they were called, 46They can assume that a newline was output just before they were called,
47and they should terminate any of their own output with a newline. 47and they should terminate any of their own output with a newline.
@@ -2242,7 +2242,7 @@ is enabled in the Help buffer."
2242 (insert (format "Minor mode%s enabled in this buffer:" 2242 (insert (format "Minor mode%s enabled in this buffer:"
2243 (if (length> local-minors 1) 2243 (if (length> local-minors 1)
2244 "s" "")))) 2244 "s" ""))))
2245 (describe-mode--minor-modes local-minors)) 2245 (describe-mode--minor-modes local-minors nil buffer))
2246 2246
2247 ;; Document the major mode. 2247 ;; Document the major mode.
2248 (let ((major (buffer-local-value 'major-mode buffer))) 2248 (let ((major (buffer-local-value 'major-mode buffer)))
@@ -2269,7 +2269,9 @@ is enabled in the Help buffer."
2269 (help-function-def--button-function 2269 (help-function-def--button-function
2270 major file-name)))))) 2270 major file-name))))))
2271 (insert ":\n\n" 2271 (insert ":\n\n"
2272 (help-split-fundoc (documentation major) nil 'doc) 2272 (help-split-fundoc (with-current-buffer buffer
2273 (documentation major))
2274 nil 'doc)
2273 (with-current-buffer buffer 2275 (with-current-buffer buffer
2274 (help-fns--list-local-commands))) 2276 (help-fns--list-local-commands)))
2275 (ensure-empty-lines 1) 2277 (ensure-empty-lines 1)
@@ -2280,7 +2282,7 @@ is enabled in the Help buffer."
2280 (insert (format "Global minor mode%s enabled:" 2282 (insert (format "Global minor mode%s enabled:"
2281 (if (length> global-minor-modes 1) 2283 (if (length> global-minor-modes 1)
2282 "s" "")))) 2284 "s" ""))))
2283 (describe-mode--minor-modes global-minor-modes t) 2285 (describe-mode--minor-modes global-minor-modes t buffer)
2284 (unless describe-mode-outline 2286 (unless describe-mode-outline
2285 (when (re-search-forward "^\f") 2287 (when (re-search-forward "^\f")
2286 (beginning-of-line) 2288 (beginning-of-line)
@@ -2297,7 +2299,7 @@ is enabled in the Help buffer."
2297 ;; For the sake of IELM and maybe others 2299 ;; For the sake of IELM and maybe others
2298 nil))))) 2300 nil)))))
2299 2301
2300(defun describe-mode--minor-modes (modes &optional global) 2302(defun describe-mode--minor-modes (modes &optional global buffer)
2301 (dolist (mode (seq-sort #'string< modes)) 2303 (dolist (mode (seq-sort #'string< modes))
2302 (let ((pretty-minor-mode 2304 (let ((pretty-minor-mode
2303 (capitalize 2305 (capitalize
@@ -2338,7 +2340,10 @@ is enabled in the Help buffer."
2338 "no indicator" 2340 "no indicator"
2339 (format "indicator%s" 2341 (format "indicator%s"
2340 indicator))))) 2342 indicator)))))
2341 (insert (or (help-split-fundoc (documentation mode) nil 'doc) 2343 (insert (or (help-split-fundoc
2344 (with-current-buffer (or buffer (current-buffer))
2345 (documentation mode))
2346 nil 'doc)
2342 "No docstring")) 2347 "No docstring"))
2343 (when describe-mode-outline 2348 (when describe-mode-outline
2344 (insert "\n\n"))))) 2349 (insert "\n\n")))))
diff --git a/lisp/help-mode.el b/lisp/help-mode.el
index f15ae633edc..47fa3590177 100644
--- a/lisp/help-mode.el
+++ b/lisp/help-mode.el
@@ -501,9 +501,13 @@ buffer after following a reference. INTERACTIVE-P is non-nil if the
501calling command was invoked interactively. In this case the stack of 501calling command was invoked interactively. In this case the stack of
502items for help buffer \"back\" buttons is cleared. 502items for help buffer \"back\" buttons is cleared.
503 503
504This should be called very early, before the output buffer is cleared, 504This function also re-enables the major mode of the buffer, thus
505because we want to record the \"previous\" position of point so we can 505resetting local variables to the values set by the mode and running the
506restore it properly when going back." 506mode hooks.
507
508So this should be called very early, before the output buffer is
509cleared, also because we want to record the \"previous\" position of
510point so we can restore it properly when going back."
507 (with-current-buffer (help-buffer) 511 (with-current-buffer (help-buffer)
508 ;; Re-enable major mode, killing all unrelated local vars. 512 ;; Re-enable major mode, killing all unrelated local vars.
509 (funcall major-mode) 513 (funcall major-mode)
diff --git a/lisp/icomplete.el b/lisp/icomplete.el
index c1d9556e24d..875c41bd841 100644
--- a/lisp/icomplete.el
+++ b/lisp/icomplete.el
@@ -79,11 +79,12 @@ selection process starts again from the user's $HOME."
79(defcustom icomplete-show-matches-on-no-input nil 79(defcustom icomplete-show-matches-on-no-input nil
80 "When non-nil, show completions when first prompting for input. 80 "When non-nil, show completions when first prompting for input.
81This means to show completions even when the current minibuffer contents 81This means to show completions even when the current minibuffer contents
82is the same as was the initial input after minibuffer activation. 82is the same as the initial input after minibuffer activation.
83This also means that if you traverse the list of completions with 83This also means that if you just hit \\`C-j' without typing any
84commands like \\`C-.' and just hit \\`RET' without typing any 84characters, this chooses the first completion candidate instead of the
85characters, the match under point will be chosen instead of the 85minibuffer's default value.
86default." 86
87See also `icomplete-ret'."
87 :type 'boolean 88 :type 'boolean
88 :version "24.4") 89 :version "24.4")
89 90
@@ -242,16 +243,25 @@ Used to implement the option `icomplete-show-matches-on-no-input'.")
242 :doc "Keymap used by `icomplete-mode' in the minibuffer." 243 :doc "Keymap used by `icomplete-mode' in the minibuffer."
243 "C-M-i" #'icomplete-force-complete 244 "C-M-i" #'icomplete-force-complete
244 "C-j" #'icomplete-force-complete-and-exit 245 "C-j" #'icomplete-force-complete-and-exit
245 "M-j" #'icomplete-exit
246 "C-." #'icomplete-forward-completions 246 "C-." #'icomplete-forward-completions
247 "C-," #'icomplete-backward-completions 247 "C-," #'icomplete-backward-completions)
248 "<remap> <minibuffer-complete-and-exit>" #'icomplete-ret)
249 248
250(defun icomplete-ret () 249(defun icomplete-ret ()
251 "Exit minibuffer for icomplete." 250 "Alternative minibuffer exit for Icomplete.
251If there is a completion candidate and the minibuffer contents is the
252same as it was right after minibuffer activation, exit selecting that
253candidate. Otherwise do as `minibuffer-complete-and-exit'.
254
255You may wish to consider binding this command to \\`RET' (or to
256`<remap> <minibuffer-complete-and-exit>') in `icomplete-minibuffer-map'.
257If you do that, then when Emacs first prompts for input such that the
258current minibuffer contents is equal to the initial input right after
259minibuffer activation, \\`RET' chooses the first completion candidate
260instead of the minibuffer's default value.
261This rebinding is especially useful if you have customized
262`icomplete-show-matches-on-no-input' to a non-nil value."
252 (interactive) 263 (interactive)
253 (if (and icomplete-show-matches-on-no-input 264 (if (and (car completion-all-sorted-completions)
254 (car completion-all-sorted-completions)
255 (equal (icomplete--field-string) icomplete--initial-input)) 265 (equal (icomplete--field-string) icomplete--initial-input))
256 (icomplete-force-complete-and-exit) 266 (icomplete-force-complete-and-exit)
257 (minibuffer-complete-and-exit))) 267 (minibuffer-complete-and-exit)))
@@ -456,8 +466,6 @@ if that doesn't produce a completion match."
456 (minibuffer-complete-and-exit) 466 (minibuffer-complete-and-exit)
457 (exit-minibuffer))) 467 (exit-minibuffer)))
458 468
459(defalias 'icomplete-exit #'icomplete-fido-exit)
460
461(defun icomplete-fido-backward-updir () 469(defun icomplete-fido-backward-updir ()
462 "Delete char before or go up directory, like `ido-mode'." 470 "Delete char before or go up directory, like `ido-mode'."
463 (interactive) 471 (interactive)
diff --git a/lisp/isearch.el b/lisp/isearch.el
index 5b5b2f0561a..b677e89c7cd 100644
--- a/lisp/isearch.el
+++ b/lisp/isearch.el
@@ -274,8 +274,6 @@ It is nil if none yet.")
274Default value, nil, means edit the string instead." 274Default value, nil, means edit the string instead."
275 :type 'boolean) 275 :type 'boolean)
276 276
277(autoload 'char-fold-to-regexp "char-fold")
278
279(defcustom search-default-mode nil 277(defcustom search-default-mode nil
280 "Default mode to use when starting isearch. 278 "Default mode to use when starting isearch.
281Value is nil, t, or a function. 279Value is nil, t, or a function.
@@ -2827,7 +2825,6 @@ With argument, add COUNT copies of the character."
2827 (mapconcat 'isearch-text-char-description 2825 (mapconcat 'isearch-text-char-description
2828 string "")))))))) 2826 string ""))))))))
2829 2827
2830(autoload 'emoji--read-emoji "emoji")
2831(defun isearch-emoji-by-name (&optional count) 2828(defun isearch-emoji-by-name (&optional count)
2832 "Read an Emoji name and add it to the search string COUNT times. 2829 "Read an Emoji name and add it to the search string COUNT times.
2833COUNT (interactively, the prefix argument) defaults to 1. 2830COUNT (interactively, the prefix argument) defaults to 1.
@@ -2835,6 +2832,7 @@ The command accepts Unicode names like \"smiling face\" or
2835\"heart with arrow\", and completion is available." 2832\"heart with arrow\", and completion is available."
2836 (interactive "p") 2833 (interactive "p")
2837 (emoji--init) 2834 (emoji--init)
2835 (declare-function emoji--read-emoji "emoji" ())
2838 (with-isearch-suspended 2836 (with-isearch-suspended
2839 (pcase-let* ((`(,glyph . ,derived) (emoji--read-emoji)) 2837 (pcase-let* ((`(,glyph . ,derived) (emoji--read-emoji))
2840 (emoji (if derived 2838 (emoji (if derived
diff --git a/lisp/json.el b/lisp/json.el
index f2086474a8b..82cc9c71bf5 100644
--- a/lisp/json.el
+++ b/lisp/json.el
@@ -609,12 +609,11 @@ transforms an unsortable MAP into a sortable alist."
609 "Insert a JSON representation of ALIST at point. 609 "Insert a JSON representation of ALIST at point.
610Sort ALIST first if `json-encoding-object-sort-predicate' is 610Sort ALIST first if `json-encoding-object-sort-predicate' is
611non-nil. Sorting can optionally be DESTRUCTIVE for speed." 611non-nil. Sorting can optionally be DESTRUCTIVE for speed."
612 (json--print-map (if (and json-encoding-object-sort-predicate alist) 612 (json--print-map (let ((pred json-encoding-object-sort-predicate))
613 (sort (if destructive alist (copy-sequence alist)) 613 (if (and pred alist)
614 (lambda (a b) 614 (sort alist :key #'car :lessp pred
615 (funcall json-encoding-object-sort-predicate 615 :in-place destructive)
616 (car a) (car b)))) 616 alist))))
617 alist)))
618 617
619;; The following two are unused but useful to keep around due to the 618;; The following two are unused but useful to keep around due to the
620;; inherent ambiguity of lists. 619;; inherent ambiguity of lists.
diff --git a/lisp/language/indian.el b/lisp/language/indian.el
index 59076faea69..d0373086fe4 100644
--- a/lisp/language/indian.el
+++ b/lisp/language/indian.el
@@ -308,6 +308,7 @@ environment."))
308 ("H" . "\u094D") ; HALANT 308 ("H" . "\u094D") ; HALANT
309 ("s" . "[\u0951\u0952]") ; stress sign 309 ("s" . "[\u0951\u0952]") ; stress sign
310 ("t" . "[\u0953\u0954]") ; accent 310 ("t" . "[\u0953\u0954]") ; accent
311 ("D" . "[\u0964\u0965]") ; punctuation sign
311 ("1" . "\u0967") ; numeral 1 312 ("1" . "\u0967") ; numeral 1
312 ("3" . "\u0969") ; numeral 3 313 ("3" . "\u0969") ; numeral 3
313 ("N" . "\u200C") ; ZWNJ 314 ("N" . "\u200C") ; ZWNJ
@@ -316,15 +317,15 @@ environment."))
316 (indian-compose-regexp 317 (indian-compose-regexp
317 (concat 318 (concat
318 ;; syllables with an independent vowel, or 319 ;; syllables with an independent vowel, or
319 "\\(?:RH\\)?Vn?\\(?:J?HR\\)?v*n?a?s?t?A?\\|" 320 "\\(?:RH\\)?Vn?\\(?:J?HR\\)?v*n?a?s?t?A?D?\\|"
320 ;; consonant-based syllables, or 321 ;; consonant-based syllables, or
321 "Cn?\\(?:J?HJ?Cn?\\)*\\(?:H[NJ]?\\|v*n?a?s?t?A?\\)\\|" 322 "Cn?\\(?:J?HJ?Cn?\\)*\\(?:H[NJ]?D?\\|v*n?a?s?t?A?D?\\)\\|"
322 ;; special consonant form, or 323 ;; special consonant form, or
323 "JHR\\|" 324 "JHRD?\\|"
324 ;; vedic accents with numerals, or 325 ;; vedic accents with numerals, or
325 "1ss?\\|3ss\\|s3ss\\|" 326 "1ss?\\|3ss\\|s3ss\\|"
326 ;; any other singleton characters 327 ;; any other singleton characters
327 "X") 328 "XD?")
328 table)) 329 table))
329 "Regexp matching a composable sequence of Devanagari characters.") 330 "Regexp matching a composable sequence of Devanagari characters.")
330 331
diff --git a/lisp/leim/quail/ipa.el b/lisp/leim/quail/ipa.el
index 2d6c6fe5a38..846c9f96f0d 100644
--- a/lisp/leim/quail/ipa.el
+++ b/lisp/leim/quail/ipa.el
@@ -74,6 +74,7 @@ Upside-down characters are obtained by a preceding slash (/)."
74 ("A~" ["ɑ̃"]) 74 ("A~" ["ɑ̃"])
75 ("oe~" ["œ̃"]) 75 ("oe~" ["œ̃"])
76 ("/c~" ["ɔ̃"]) 76 ("/c~" ["ɔ̃"])
77 ("/E" ?ɜ)
77 ("p" ?p) 78 ("p" ?p)
78 ("b" ?b) 79 ("b" ?b)
79 ("t" ?t) 80 ("t" ?t)
diff --git a/lisp/leim/quail/iroquoian.el b/lisp/leim/quail/iroquoian.el
index 0bd822217b3..38b53f39483 100644
--- a/lisp/leim/quail/iroquoian.el
+++ b/lisp/leim/quail/iroquoian.el
@@ -24,7 +24,7 @@
24 24
25;; This file implements input methods for Northern Iroquoian languages. 25;; This file implements input methods for Northern Iroquoian languages.
26 26
27;; Input methods are implemented for all Five Nations Iroquois 27;; Input methods are implemented for the following Northern Iroquoian
28;; languages: 28;; languages:
29 29
30;; - Mohawk (Kanien’kéha / Kanyen’kéha / Onkwehonwehnéha) 30;; - Mohawk (Kanien’kéha / Kanyen’kéha / Onkwehonwehnéha)
@@ -32,6 +32,7 @@
32;; - Onondaga (Onųdaʔgegáʔ) 32;; - Onondaga (Onųdaʔgegáʔ)
33;; - Cayuga (Gayogo̱ho:nǫhnéha:ˀ) 33;; - Cayuga (Gayogo̱ho:nǫhnéha:ˀ)
34;; - Seneca (Onödowá’ga:’) 34;; - Seneca (Onödowá’ga:’)
35;; - Tuscarora (Skarù·ręʔ)
35 36
36;; A composite input method for all of the languages above is also 37;; A composite input method for all of the languages above is also
37;; defined: `haudenosaunee-postfix'. 38;; defined: `haudenosaunee-postfix'.
@@ -39,7 +40,6 @@
39;; Input methods are not yet implemented for the remaining Northern 40;; Input methods are not yet implemented for the remaining Northern
40;; Iroquoian languages, including: 41;; Iroquoian languages, including:
41 42
42;; - Tuscarora (Skarù:ręʔ)
43;; - Wendat (Huron) / Wyandot 43;; - Wendat (Huron) / Wyandot
44 44
45;;; Code: 45;;; Code:
@@ -799,6 +799,159 @@ simultaneously using the input method `haudenosaunee-postfix'."
799 (quail-defrule key trans)) 799 (quail-defrule key trans))
800 800
801 801
802;;; Tuscarora
803
804;;
805;; The primary community orthography used for Tuscarora follows that
806;; used in Blair Rudes's dictionary (see below).
807;;
808;; Reference work for Tuscarora orthography:
809;;
810;; Blair Rudes. 1999. Tuscarora-English/English-Tuscarora
811;; dictionary. Toronto: University of Toronto Press.
812;;
813
814(defconst iroquoian-tuscarora-modifier-alist
815 '(("::" ?\N{MIDDLE DOT}))
816 "Alist of rules for modifier letters in Tuscarora input methods.
817Entries are as with rules in `quail-define-rules'.")
818
819(defconst iroquoian-tuscarora-vowel-alist
820 '(("a'" ?á)
821 ("a`" ?à)
822 ("A'" ?Á)
823 ("A`" ?À)
824 ("e'" ?é)
825 ("e`" ?è)
826 ("E'" ?É)
827 ("E`" ?È)
828 ("i'" ?í)
829 ("i`" ?ì)
830 ("I'" ?Í)
831 ("I`" ?Ì)
832 ("u'" ?ú)
833 ("u`" ?ù)
834 ("U'" ?Ú)
835 ("U`" ?Ù)
836 ("e," ?ę)
837 ("e,'" ["ę́"])
838 ("e,`" ["ę̀"])
839 ("E," ?Ę)
840 ("E,'" ["Ę́"])
841 ("E,`" ["Ę̀"])
842
843 ("a''" ["a'"])
844 ("a``" ["a`"])
845 ("A''" ["A'"])
846 ("A``" ["A`"])
847 ("e''" ["e'"])
848 ("e``" ["e`"])
849 ("E''" ["E'"])
850 ("E``" ["E`"])
851 ("i''" ["i'"])
852 ("i``" ["i`"])
853 ("I''" ["I'"])
854 ("I``" ["I`"])
855 ("u''" ["u'"])
856 ("u``" ["u`"])
857 ("U''" ["U'"])
858 ("U``" ["U`"])
859
860 ("e,," ["e,"])
861 ("e,''" ["ę'"])
862 ("e,``" ["ę`"])
863 ("E,," ["E,"])
864 ("E,''" ["Ę'"])
865 ("E,``" ["Ę`"]))
866 "Alist of rules for vowel letters in Tuscarora input methods.
867Entries are as with rules in `quail-define-rules'.")
868
869(defconst iroquoian-tuscarora-consonant-alist
870 '((";;" ?\N{LATIN LETTER GLOTTAL STOP})
871 ("c/" ?č)
872 ("c//" ["c/"])
873 ("C/" ?Č)
874 ("C//" ["C/"])
875 ("t/" ?θ)
876 ("t//" ["t/"]))
877 "Alist of rules for consonant letters in Tuscarora input methods.
878Entries are as with rules in `quail-define-rules'.")
879
880(defconst iroquoian-tuscarora-exception-alist
881 '(("_" ?\N{COMBINING LOW LINE})
882 ("__" ?_))
883 "Alist of rules for phonological exception marking in Tuscarora input methods.
884Entries are as with rules in `quail-define-rules'.")
885
886(quail-define-package
887 "tuscarora-postfix" "Tuscarora" "TUS<" t
888 "Tuscarora (Skarù·ręʔ) input method with postfix modifiers
889
890Modifiers:
891
892| Key | Translation | Description |
893|-----+-------------+--------------------------|
894| :: | · | Vowel length |
895
896Stress diacritics:
897
898| Key | Description | Example |
899|------+--------------+---------|
900| \\=' | Acute accent | a' -> á |
901| \\=` | Grave accent | a` -> à |
902
903Doubling the postfix separates the letter and the postfix.
904
905Vowels:
906
907| Key | Translation | Description |
908|-----+-------------+---------------------------------|
909| e, | ę | Mid front nasal vowel |
910| E, | Ę | Mid front nasal vowel (capital) |
911
912a, e, i, and u are bound to a single key.
913
914Consonants:
915
916| Key | Translation | Description |
917|-------+-------------+------------------------------------|
918| ;; | ˀ | Glottal stop |
919| c/ | č | Postalveolar affricate |
920| C/ | Č | Postalveolar affricate (capital) |
921| t/ | θ | Voiceless dental fricative |
922
923h, k, n, r, s, t, w, and y are bound to a single key.
924
925b, l, m, and p are used rarely in loanwords. They are also each bound
926to a single key.
927
928Stress exception markers:
929
930| Key | Description | Example |
931|-----+--------------------+----------|
932| _ | Combining low line | a_ -> a̲ |
933
934Note: Not all fonts can properly display a combining low line on all
935letters.
936
937Underlining has been used by some to indicate that vowels behave
938exceptionally with regard to stress placement. Alternatively, markup or
939other methods can be used to create an underlining effect.
940
941To enter a plain underscore, type the underscore twice.
942
943All Haudenosaunee languages, including Tuscarora can be input
944simultaneously using the input method `haudenosaunee-postfix'."
945 nil t nil nil nil nil nil nil nil nil t)
946
947(pcase-dolist (`(,key ,trans)
948 (append iroquoian-tuscarora-modifier-alist
949 iroquoian-tuscarora-consonant-alist
950 iroquoian-tuscarora-vowel-alist
951 iroquoian-tuscarora-exception-alist))
952 (quail-defrule key trans))
953
954
802;;; Haudenosaunee (composite Northern Iroquoian) 955;;; Haudenosaunee (composite Northern Iroquoian)
803 956
804;; 957;;
@@ -857,7 +1010,8 @@ simultaneously using the input method `haudenosaunee-postfix'."
857 iroquoian-oneida-modifier-alist 1010 iroquoian-oneida-modifier-alist
858 iroquoian-onondaga-modifier-alist 1011 iroquoian-onondaga-modifier-alist
859 iroquoian-cayuga-modifier-alist 1012 iroquoian-cayuga-modifier-alist
860 iroquoian-seneca-modifier-alist)) 1013 iroquoian-seneca-modifier-alist
1014 iroquoian-tuscarora-modifier-alist))
861 "Alist of rules for modifier letters in Haudenosaunee input methods. 1015 "Alist of rules for modifier letters in Haudenosaunee input methods.
862Entries are as with rules in `quail-define-rules'.") 1016Entries are as with rules in `quail-define-rules'.")
863 1017
@@ -866,7 +1020,8 @@ Entries are as with rules in `quail-define-rules'.")
866 iroquoian-oneida-vowel-alist 1020 iroquoian-oneida-vowel-alist
867 iroquoian-onondaga-vowel-alist 1021 iroquoian-onondaga-vowel-alist
868 iroquoian-cayuga-vowel-alist 1022 iroquoian-cayuga-vowel-alist
869 iroquoian-seneca-vowel-alist)) 1023 iroquoian-seneca-vowel-alist
1024 iroquoian-tuscarora-vowel-alist))
870 "Alist of rules for vowel letters in Haudenosaunee input methods. 1025 "Alist of rules for vowel letters in Haudenosaunee input methods.
871Entries are as with rules in `quail-define-rules'.") 1026Entries are as with rules in `quail-define-rules'.")
872 1027
@@ -879,16 +1034,17 @@ Entries are as with rules in `quail-define-rules'.")
879 iroquoian-oneida-consonant-alist 1034 iroquoian-oneida-consonant-alist
880 iroquoian-onondaga-consonant-alist 1035 iroquoian-onondaga-consonant-alist
881 iroquoian-cayuga-consonant-alist 1036 iroquoian-cayuga-consonant-alist
882 iroquoian-seneca-consonant-alist) 1037 iroquoian-seneca-consonant-alist
1038 iroquoian-tuscarora-consonant-alist)
883 (lambda (c1 c2) 1039 (lambda (c1 c2)
884 (equal (car c1) (car c2)))) 1040 (equal (car c1) (car c2))))
885 "Alist of rules for consonant letters in Haudenosaunee input methods. 1041 "Alist of rules for consonant letters in Haudenosaunee input methods.
886Entries are as with rules in `quail-define-rules'.") 1042Entries are as with rules in `quail-define-rules'.")
887 1043
888(defconst iroquoian-haudenosaunee-devoicing-alist 1044(defconst iroquoian-haudenosaunee-exception-alist
889 '(("_" ?\N{COMBINING LOW LINE}) 1045 '(("_" ?\N{COMBINING LOW LINE})
890 ("__" ?_)) 1046 ("__" ?_))
891 "Alist of rules for devoicing characters in Haudenosaunee input methods. 1047 "Rules alist for phonological exception markers in Haudenosaunee input methods.
892Entries are as with rules in `quail-define-rules'.") 1048Entries are as with rules in `quail-define-rules'.")
893 1049
894(defconst iroquoian-haudenosaunee-nasal-alist iroquoian-onondaga-nasal-alist 1050(defconst iroquoian-haudenosaunee-nasal-alist iroquoian-onondaga-nasal-alist
@@ -906,6 +1062,7 @@ This input method can be used to enter the following languages:
906- Cayuga (Gayogo̱ho:nǫhnéha:ˀ) 1062- Cayuga (Gayogo̱ho:nǫhnéha:ˀ)
907- Onondaga (Onųdaʔgegáʔ) 1063- Onondaga (Onųdaʔgegáʔ)
908- Seneca (Onödowá’ga:’) 1064- Seneca (Onödowá’ga:’)
1065- Tuscarora (Skarù·ręʔ)
909 1066
910Modifiers: 1067Modifiers:
911 1068
@@ -989,6 +1146,12 @@ Vowels:
989| a\" | ä | Low front vowel | 1146| a\" | ä | Low front vowel |
990| A\" | Ä | Low front vowel (capital) | 1147| A\" | Ä | Low front vowel (capital) |
991| Single-key vowels: a e i o u | 1148| Single-key vowels: a e i o u |
1149|----------------------------------------------------------------------|
1150| Tuscarora |
1151| -------------------------------------------------------------------- |
1152| e, | ę | Mid front nasal vowel |
1153| E, | Ę | Mid front nasal vowel (capital) |
1154| Single-key vowels: a e i u |
992 1155
993Consonants: 1156Consonants:
994 1157
@@ -1023,8 +1186,16 @@ Consonants:
1023| s/ | š | Voiceless postalveolar fricative | 1186| s/ | š | Voiceless postalveolar fricative |
1024| S/ | Š | Voiceless postalveolar fricative (capital) | 1187| S/ | Š | Voiceless postalveolar fricative (capital) |
1025| Single-key consonants: d g h j k n s t w y z (b m p) | 1188| Single-key consonants: d g h j k n s t w y z (b m p) |
1189|----------------------------------------------------------------------|
1190| Tuscarora |
1191| -------------------------------------------------------------------- |
1192| ;: | ʔ | Glottal stop (alternate) |
1193| c/ | č | Postalveolar affricate |
1194| C/ | Č | Postalveolar affricate (capital) |
1195| t/ | θ | Voiceless dental fricative |
1196| Single-key consonants: h k n r s t w y (b l m p) |
1026 1197
1027Devoicing: 1198Phonological exception markers:
1028 1199
1029| Key | Description | Examples | 1200| Key | Description | Examples |
1030|-----+------------------------+------------------------------| 1201|-----+------------------------+------------------------------|
@@ -1035,8 +1206,10 @@ Note: Not all fonts can properly display a combining low line on all
1035letters and a combining macron below on all vowels. 1206letters and a combining macron below on all vowels.
1036 1207
1037Underlining is commonly used in Oneida to indicate devoiced syllables on 1208Underlining is commonly used in Oneida to indicate devoiced syllables on
1038pre-pausal forms (also called utterance-final forms). Alternatively, 1209pre-pausal forms (also called utterance-final forms), and it has been
1039markup or other methods can be used to create an underlining effect. 1210used in some Tuscarora orthographies to indicate that vowels behave
1211exceptionally with regard to stress placement. Alternatively, markup or
1212other methods can be used to create an underlining effect.
1040 1213
1041To enter a plain underscore, the underscore twice. 1214To enter a plain underscore, the underscore twice.
1042 1215
@@ -1046,7 +1219,8 @@ To enter a plain hyphen after a vowel, simply type the hyphen twice.
1046 1219
1047There are individual input methods for each of the languages that can be 1220There are individual input methods for each of the languages that can be
1048entered with this input method: `mohawk-postfix', `oneida-postfix', 1221entered with this input method: `mohawk-postfix', `oneida-postfix',
1049`onondaga-postfix', `cayuga-postfix', `seneca-postfix'." 1222`onondaga-postfix', `cayuga-postfix', `seneca-postfix',
1223`tuscarora-postfix'.."
1050 nil t nil nil nil nil nil nil nil nil t) 1224 nil t nil nil nil nil nil nil nil nil t)
1051 1225
1052(pcase-dolist (`(,key ,trans) 1226(pcase-dolist (`(,key ,trans)
@@ -1054,7 +1228,7 @@ entered with this input method: `mohawk-postfix', `oneida-postfix',
1054 iroquoian-haudenosaunee-consonant-alist 1228 iroquoian-haudenosaunee-consonant-alist
1055 iroquoian-haudenosaunee-nasal-alist 1229 iroquoian-haudenosaunee-nasal-alist
1056 iroquoian-haudenosaunee-vowel-alist 1230 iroquoian-haudenosaunee-vowel-alist
1057 iroquoian-haudenosaunee-devoicing-alist)) 1231 iroquoian-haudenosaunee-exception-alist))
1058 (quail-defrule key trans)) 1232 (quail-defrule key trans))
1059 1233
1060(provide 'iroquoian) 1234(provide 'iroquoian)
diff --git a/lisp/net/dbus.el b/lisp/net/dbus.el
index 1c8f329fdd7..465de028725 100644
--- a/lisp/net/dbus.el
+++ b/lisp/net/dbus.el
@@ -319,6 +319,10 @@ If the parameter `:authorizable' is given and the following AUTH
319is non-nil, the invoked method may interactively prompt the user 319is non-nil, the invoked method may interactively prompt the user
320for authorization. The default is nil. 320for authorization. The default is nil.
321 321
322If the parameter `:keep-fd' is given, and the return message has a first
323argument with a D-Bus type `:unix-fd', the returned file desriptor is
324kept internally, and can be used in a later `dbus--close-fd' call.
325
322All other arguments ARGS are passed to METHOD as arguments. They are 326All other arguments ARGS are passed to METHOD as arguments. They are
323converted into D-Bus types via the following rules: 327converted into D-Bus types via the following rules:
324 328
@@ -453,6 +457,10 @@ If the parameter `:authorizable' is given and the following AUTH
453is non-nil, the invoked method may interactively prompt the user 457is non-nil, the invoked method may interactively prompt the user
454for authorization. The default is nil. 458for authorization. The default is nil.
455 459
460If the parameter `:keep-fd' is given, and the return message has a first
461argument with a D-Bus type `:unix-fd', the returned file desriptor is
462kept internally, and can be used in a later `dbus--close-fd' call.
463
456All other arguments ARGS are passed to METHOD as arguments. They are 464All other arguments ARGS are passed to METHOD as arguments. They are
457converted into D-Bus types via the following rules: 465converted into D-Bus types via the following rules:
458 466
@@ -604,6 +612,7 @@ This is an internal function, it shall not be used outside dbus.el."
604 612
605;;; Hash table of registered functions. 613;;; Hash table of registered functions.
606 614
615;; Seems to be unused. Dow we want to keep it?
607(defun dbus-list-hash-table () 616(defun dbus-list-hash-table ()
608 "Return all registered member registrations to D-Bus. 617 "Return all registered member registrations to D-Bus.
609The return value is a list, with elements of kind (KEY . VALUE). 618The return value is a list, with elements of kind (KEY . VALUE).
@@ -613,7 +622,7 @@ hash table."
613 (maphash 622 (maphash
614 (lambda (key value) (push (cons key value) result)) 623 (lambda (key value) (push (cons key value) result))
615 dbus-registered-objects-table) 624 dbus-registered-objects-table)
616 result)) 625 (nreverse result)))
617 626
618(defun dbus-setenv (bus variable value) 627(defun dbus-setenv (bus variable value)
619 "Set the value of the BUS environment variable named VARIABLE to VALUE. 628 "Set the value of the BUS environment variable named VARIABLE to VALUE.
@@ -2098,6 +2107,7 @@ either a method name, a signal name, or an error name."
2098 2107
2099(defun dbus-monitor-goto-serial () 2108(defun dbus-monitor-goto-serial ()
2100 "Goto D-Bus message with the same serial number." 2109 "Goto D-Bus message with the same serial number."
2110 (declare (completion ignore))
2101 (interactive) 2111 (interactive)
2102 (when (mouse-event-p last-input-event) (mouse-set-point last-input-event)) 2112 (when (mouse-event-p last-input-event) (mouse-set-point last-input-event))
2103 (when-let* ((point (get-text-property (point) 'dbus-serial))) 2113 (when-let* ((point (get-text-property (point) 'dbus-serial)))
diff --git a/lisp/net/newst-backend.el b/lisp/net/newst-backend.el
index babd55fb29d..58bbb1b7fcb 100644
--- a/lisp/net/newst-backend.el
+++ b/lisp/net/newst-backend.el
@@ -1109,12 +1109,13 @@ same as in `newsticker--parse-atom-1.0'."
1109 1109
1110(defun newsticker--parse-text-container (node) 1110(defun newsticker--parse-text-container (node)
1111 "Handle content according to ``type'' attribute." 1111 "Handle content according to ``type'' attribute."
1112 (let ((content (car (xml-node-children node)))) 1112 (let ((content (car (xml-node-children node)))
1113 (if (string= "html" (xml-get-attribute node 'type)) 1113 (type (xml-get-attribute node 'type)))
1114 ;; element contains entity escaped html 1114 (if (string= "xhtml" type)
1115 content 1115 ;; xhtml: reverse-parse xml nodes back to string
1116 ;; plain text or xhtml 1116 (newsticker--unxml content)
1117 (newsticker--unxml content)))) 1117 ;; plain text (default) or entity-escaped html: return as-is
1118 content)))
1118 1119
1119(defun newsticker--unxml (node) 1120(defun newsticker--unxml (node)
1120 "Reverse parsing of an xml string. 1121 "Reverse parsing of an xml string.
diff --git a/lisp/net/shr.el b/lisp/net/shr.el
index bf78cce13bf..517cb3cc237 100644
--- a/lisp/net/shr.el
+++ b/lisp/net/shr.el
@@ -2731,7 +2731,7 @@ flags that control whether to collect or render objects."
2731 (aref widths width-column) 2731 (aref widths width-column)
2732 (* 10 shr-table-separator-pixel-width))) 2732 (* 10 shr-table-separator-pixel-width)))
2733 (when (setq colspan (dom-attr column 'colspan)) 2733 (when (setq colspan (dom-attr column 'colspan))
2734 (setq colspan (min (string-to-number colspan) 2734 (setq colspan (min (truncate (string-to-number colspan))
2735 ;; The colspan may be wrong, so 2735 ;; The colspan may be wrong, so
2736 ;; truncate it to the length of the 2736 ;; truncate it to the length of the
2737 ;; remaining columns. 2737 ;; remaining columns.
diff --git a/lisp/net/tramp-adb.el b/lisp/net/tramp-adb.el
index 5bcb92536fd..c20b5df9b59 100644
--- a/lisp/net/tramp-adb.el
+++ b/lisp/net/tramp-adb.el
@@ -974,7 +974,7 @@ E.g. a host name \"192.168.1.1#5555\" returns \"192.168.1.1:5555\"
974 (sleep-for 0.1) 974 (sleep-for 0.1)
975 host) 975 host)
976 (t (tramp-error 976 (t (tramp-error
977 vec 'file-error "Could not find device %s" host))))))) 977 vec 'remote-file-error "Could not find device %s" host)))))))
978 978
979(defun tramp-adb-execute-adb-command (vec &rest args) 979(defun tramp-adb-execute-adb-command (vec &rest args)
980 "Execute an adb command. 980 "Execute an adb command.
@@ -1047,7 +1047,7 @@ the exit status."
1047 (with-current-buffer (tramp-get-connection-buffer vec) 1047 (with-current-buffer (tramp-get-connection-buffer vec)
1048 (unless (tramp-search-regexp (rx "tramp_exit_status " (+ digit))) 1048 (unless (tramp-search-regexp (rx "tramp_exit_status " (+ digit)))
1049 (tramp-error 1049 (tramp-error
1050 vec 'file-error "Couldn't find exit status of `%s'" command)) 1050 vec 'remote-file-error "Couldn't find exit status of `%s'" command))
1051 (skip-chars-forward "^ ") 1051 (skip-chars-forward "^ ")
1052 (prog1 1052 (prog1
1053 (if exit-status 1053 (if exit-status
@@ -1060,13 +1060,14 @@ the exit status."
1060 "Run COMMAND, check exit status, throw error if exit status not okay. 1060 "Run COMMAND, check exit status, throw error if exit status not okay.
1061FMT and ARGS are passed to `error'." 1061FMT and ARGS are passed to `error'."
1062 (unless (tramp-adb-send-command-and-check vec command) 1062 (unless (tramp-adb-send-command-and-check vec command)
1063 (apply #'tramp-error vec 'file-error fmt args))) 1063 (apply #'tramp-error vec 'remote-file-error fmt args)))
1064 1064
1065(defun tramp-adb-wait-for-output (proc &optional timeout) 1065(defun tramp-adb-wait-for-output (proc &optional timeout)
1066 "Wait for output from remote command." 1066 "Wait for output from remote command."
1067 (unless (buffer-live-p (process-buffer proc)) 1067 (unless (buffer-live-p (process-buffer proc))
1068 (delete-process proc) 1068 (delete-process proc)
1069 (tramp-error proc 'file-error "Process `%s' not available, try again" proc)) 1069 (tramp-error
1070 proc 'remote-file-error "Process `%s' not available, try again" proc))
1070 (let ((prompt (tramp-get-connection-property proc "prompt" tramp-adb-prompt))) 1071 (let ((prompt (tramp-get-connection-property proc "prompt" tramp-adb-prompt)))
1071 (with-current-buffer (process-buffer proc) 1072 (with-current-buffer (process-buffer proc)
1072 (if (tramp-wait-for-regexp proc timeout prompt) 1073 (if (tramp-wait-for-regexp proc timeout prompt)
@@ -1085,10 +1086,11 @@ FMT and ARGS are passed to `error'."
1085 (delete-region (point) (point-max)))) 1086 (delete-region (point) (point-max))))
1086 (if timeout 1087 (if timeout
1087 (tramp-error 1088 (tramp-error
1088 proc 'file-error 1089 proc 'remote-file-error
1089 "[[Remote prompt `%s' not found in %d secs]]" prompt timeout) 1090 "[[Remote prompt `%s' not found in %d secs]]" prompt timeout)
1090 (tramp-error 1091 (tramp-error
1091 proc 'file-error "[[Remote prompt `%s' not found]]" prompt)))))) 1092 proc 'remote-file-error
1093 "[[Remote prompt `%s' not found]]" prompt))))))
1092 1094
1093(defun tramp-adb-maybe-open-connection (vec) 1095(defun tramp-adb-maybe-open-connection (vec)
1094 "Maybe open a connection VEC. 1096 "Maybe open a connection VEC.
@@ -1110,13 +1112,14 @@ connection if a previous connection has died for some reason."
1110 ;; whether it is still the same device. 1112 ;; whether it is still the same device.
1111 (when 1113 (when
1112 (and user (not (tramp-get-connection-property vec " su-command-p" t))) 1114 (and user (not (tramp-get-connection-property vec " su-command-p" t)))
1113 (tramp-error vec 'file-error "Cannot switch to user `%s'" user)) 1115 (tramp-error vec 'remote-file-error "Cannot switch to user `%s'" user))
1114 1116
1115 (unless (process-live-p p) 1117 (unless (process-live-p p)
1116 (save-match-data 1118 (save-match-data
1117 (when (and p (processp p)) (delete-process p)) 1119 (when (and p (processp p)) (delete-process p))
1118 (if (tramp-string-empty-or-nil-p device) 1120 (if (tramp-string-empty-or-nil-p device)
1119 (tramp-error vec 'file-error "Device %s not connected" host)) 1121 (tramp-error
1122 vec 'remote-file-error "Device %s not connected" host))
1120 (with-tramp-progress-reporter vec 3 "Opening adb shell connection" 1123 (with-tramp-progress-reporter vec 3 "Opening adb shell connection"
1121 (let* ((coding-system-for-read 'utf-8-dos) ; Is this correct? 1124 (let* ((coding-system-for-read 'utf-8-dos) ; Is this correct?
1122 (process-connection-type tramp-process-connection-type) 1125 (process-connection-type tramp-process-connection-type)
@@ -1137,7 +1140,7 @@ connection if a previous connection has died for some reason."
1137 (tramp-send-string vec tramp-rsh-end-of-line) 1140 (tramp-send-string vec tramp-rsh-end-of-line)
1138 (tramp-adb-wait-for-output p 30) 1141 (tramp-adb-wait-for-output p 30)
1139 (unless (process-live-p p) 1142 (unless (process-live-p p)
1140 (tramp-error vec 'file-error "Terminated!")) 1143 (tramp-error vec 'remote-file-error "Terminated!"))
1141 1144
1142 ;; Set connection-local variables. 1145 ;; Set connection-local variables.
1143 (tramp-set-connection-local-variables vec) 1146 (tramp-set-connection-local-variables vec)
@@ -1193,7 +1196,7 @@ connection if a previous connection has died for some reason."
1193 ;; Do not flush, we need the nil value. 1196 ;; Do not flush, we need the nil value.
1194 (tramp-set-connection-property vec " su-command-p" nil) 1197 (tramp-set-connection-property vec " su-command-p" nil)
1195 (tramp-error 1198 (tramp-error
1196 vec 'file-error "Cannot switch to user `%s'" user))) 1199 vec 'remote-file-error "Cannot switch to user `%s'" user)))
1197 1200
1198 ;; Mark it as connected. 1201 ;; Mark it as connected.
1199 (tramp-set-connection-property p "connected" t)))))))) 1202 (tramp-set-connection-property p "connected" t))))))))
diff --git a/lisp/net/tramp-archive.el b/lisp/net/tramp-archive.el
index a4323156c2a..e970fd1cd56 100644
--- a/lisp/net/tramp-archive.el
+++ b/lisp/net/tramp-archive.el
@@ -737,7 +737,7 @@ offered."
737 (apply #'tramp-archive-file-name-for-operation operation args))))) 737 (apply #'tramp-archive-file-name-for-operation operation args)))))
738 (tramp-message v 10 "%s" (cons operation args)) 738 (tramp-message v 10 "%s" (cons operation args))
739 (tramp-error 739 (tramp-error
740 v 'file-error 740 v 'remote-file-error
741 "Operation `%s' not implemented for file archives" operation))) 741 "Operation `%s' not implemented for file archives" operation)))
742 742
743(add-hook 'tramp-unload-hook 743(add-hook 'tramp-unload-hook
diff --git a/lisp/net/tramp-crypt.el b/lisp/net/tramp-crypt.el
index 565b9f0a5aa..59e4cea2edb 100644
--- a/lisp/net/tramp-crypt.el
+++ b/lisp/net/tramp-crypt.el
@@ -446,7 +446,7 @@ Otherwise, return NAME."
446 crypt-vec (if (eq op 'encrypt) "encode" "decode") 446 crypt-vec (if (eq op 'encrypt) "encode" "decode")
447 tramp-compat-temporary-file-directory localname) 447 tramp-compat-temporary-file-directory localname)
448 (tramp-error 448 (tramp-error
449 crypt-vec 'file-error "%s of file name %s failed" 449 crypt-vec 'remote-file-error "%s of file name %s failed"
450 (if (eq op 'encrypt) "Encoding" "Decoding") name)) 450 (if (eq op 'encrypt) "Encoding" "Decoding") name))
451 (with-current-buffer (tramp-get-connection-buffer crypt-vec) 451 (with-current-buffer (tramp-get-connection-buffer crypt-vec)
452 (goto-char (point-min)) 452 (goto-char (point-min))
@@ -481,7 +481,7 @@ Raise an error if this fails."
481 (file-name-directory infile) 481 (file-name-directory infile)
482 (concat "/" (file-name-nondirectory infile))) 482 (concat "/" (file-name-nondirectory infile)))
483 (tramp-error 483 (tramp-error
484 crypt-vec 'file-error "%s of file %s failed" 484 crypt-vec 'remote-file-error "%s of file %s failed"
485 (if (eq op 'encrypt) "Encrypting" "Decrypting") infile)) 485 (if (eq op 'encrypt) "Encrypting" "Decrypting") infile))
486 (with-current-buffer (tramp-get-connection-buffer crypt-vec) 486 (with-current-buffer (tramp-get-connection-buffer crypt-vec)
487 (write-region nil nil outfile))))) 487 (write-region nil nil outfile)))))
diff --git a/lisp/net/tramp-gvfs.el b/lisp/net/tramp-gvfs.el
index 64efce227d6..0f68e4d768a 100644
--- a/lisp/net/tramp-gvfs.el
+++ b/lisp/net/tramp-gvfs.el
@@ -1006,7 +1006,7 @@ The global value will always be nil; it is bound where needed.")
1006 "Called when a D-Bus error message arrives, see `dbus-event-error-functions'." 1006 "Called when a D-Bus error message arrives, see `dbus-event-error-functions'."
1007 (when tramp-gvfs-dbus-event-vector 1007 (when tramp-gvfs-dbus-event-vector
1008 (tramp-message tramp-gvfs-dbus-event-vector 6 "%S" event) 1008 (tramp-message tramp-gvfs-dbus-event-vector 6 "%S" event)
1009 (tramp-error tramp-gvfs-dbus-event-vector 'file-error (cadr err)))) 1009 (tramp-error tramp-gvfs-dbus-event-vector 'remote-file-error (cadr err))))
1010 1010
1011(add-hook 'dbus-event-error-functions #'tramp-gvfs-dbus-event-error) 1011(add-hook 'dbus-event-error-functions #'tramp-gvfs-dbus-event-error)
1012(add-hook 'tramp-gvfs-unload-hook 1012(add-hook 'tramp-gvfs-unload-hook
@@ -2234,7 +2234,7 @@ connection if a previous connection has died for some reason."
2234 method) 2234 method)
2235 tramp-gvfs-mounttypes) 2235 tramp-gvfs-mounttypes)
2236 (tramp-error 2236 (tramp-error
2237 vec 'file-error "Method `%s' not supported by GVFS" method))) 2237 vec 'remote-file-error "Method `%s' not supported by GVFS" method)))
2238 2238
2239 ;; For password handling, we need a process bound to the 2239 ;; For password handling, we need a process bound to the
2240 ;; connection buffer. Therefore, we create a dummy process. 2240 ;; connection buffer. Therefore, we create a dummy process.
@@ -2332,10 +2332,10 @@ connection if a previous connection has died for some reason."
2332 vec 'tramp-connection-timeout tramp-connection-timeout) 2332 vec 'tramp-connection-timeout tramp-connection-timeout)
2333 (if (tramp-string-empty-or-nil-p user-domain) 2333 (if (tramp-string-empty-or-nil-p user-domain)
2334 (tramp-error 2334 (tramp-error
2335 vec 'file-error 2335 vec 'remote-file-error
2336 "Timeout reached mounting %s using %s" host-port method) 2336 "Timeout reached mounting %s using %s" host-port method)
2337 (tramp-error 2337 (tramp-error
2338 vec 'file-error 2338 vec 'remote-file-error
2339 "Timeout reached mounting %s@%s using %s" 2339 "Timeout reached mounting %s@%s using %s"
2340 user-domain host-port method))) 2340 user-domain host-port method)))
2341 (while (not (tramp-get-file-property vec "/" "fuse-mountpoint")) 2341 (while (not (tramp-get-file-property vec "/" "fuse-mountpoint"))
@@ -2345,7 +2345,7 @@ connection if a previous connection has died for some reason."
2345 ;; is marked with the fuse-mountpoint "/". We shall react. 2345 ;; is marked with the fuse-mountpoint "/". We shall react.
2346 (when (string-equal 2346 (when (string-equal
2347 (tramp-get-file-property vec "/" "fuse-mountpoint" "") "/") 2347 (tramp-get-file-property vec "/" "fuse-mountpoint" "") "/")
2348 (tramp-error vec 'file-error "FUSE mount denied")) 2348 (tramp-error vec 'remote-file-error "FUSE mount denied"))
2349 2349
2350 ;; Save the password. 2350 ;; Save the password.
2351 (ignore-errors 2351 (ignore-errors
diff --git a/lisp/net/tramp-rclone.el b/lisp/net/tramp-rclone.el
index 6b0daeba2ac..cd5c3f46f54 100644
--- a/lisp/net/tramp-rclone.el
+++ b/lisp/net/tramp-rclone.el
@@ -381,53 +381,53 @@ connection if a previous connection has died for some reason."
381 381
382 (with-tramp-debug-message vec "Opening connection" 382 (with-tramp-debug-message vec "Opening connection"
383 (let ((host (tramp-file-name-host vec))) 383 (let ((host (tramp-file-name-host vec)))
384 (when (rassoc `(,host) (tramp-rclone-parse-device-names nil)) 384 (when (or (tramp-string-empty-or-nil-p host)
385 (if (tramp-string-empty-or-nil-p host) 385 (not (rassoc `(,host) (tramp-rclone-parse-device-names nil))))
386 (tramp-error vec 'file-error "Storage %s not connected" host)) 386 (tramp-error vec 'remote-file-error "Storage %s not connected" host))
387 ;; We need a process bound to the connection buffer. 387
388 ;; Therefore, we create a dummy process. Maybe there is a 388 ;; We need a process bound to the connection buffer. Therefore,
389 ;; better solution? 389 ;; we create a dummy process. Maybe there is a better solution?
390 (unless (get-buffer-process (tramp-get-connection-buffer vec)) 390 (unless (get-buffer-process (tramp-get-connection-buffer vec))
391 (let ((p (make-network-process 391 (let ((p (make-network-process
392 :name (tramp-get-connection-name vec) 392 :name (tramp-get-connection-name vec)
393 :buffer (tramp-get-connection-buffer vec) 393 :buffer (tramp-get-connection-buffer vec)
394 :server t :host 'local :service t :noquery t))) 394 :server t :host 'local :service t :noquery t)))
395 (tramp-post-process-creation p vec) 395 (tramp-post-process-creation p vec)
396 396
397 ;; Set connection-local variables. 397 ;; Set connection-local variables.
398 (tramp-set-connection-local-variables vec))) 398 (tramp-set-connection-local-variables vec)))
399 399
400 ;; Create directory. 400 ;; Create directory.
401 (unless (file-directory-p (tramp-fuse-mount-point vec)) 401 (unless (file-directory-p (tramp-fuse-mount-point vec))
402 (make-directory (tramp-fuse-mount-point vec) 'parents)) 402 (make-directory (tramp-fuse-mount-point vec) 'parents))
403 403
404 ;; Mount. This command does not return, so we use 0 as 404 ;; Mount. This command does not return, so we use 0 as
405 ;; DESTINATION of `tramp-call-process'. 405 ;; DESTINATION of `tramp-call-process'.
406 (unless (tramp-fuse-mounted-p vec) 406 (unless (tramp-fuse-mounted-p vec)
407 (apply 407 (apply
408 #'tramp-call-process 408 #'tramp-call-process
409 vec tramp-rclone-program nil 0 nil 409 vec tramp-rclone-program nil 0 nil
410 "mount" (tramp-fuse-mount-spec vec) 410 "mount" (tramp-fuse-mount-spec vec)
411 (tramp-fuse-mount-point vec) 411 (tramp-fuse-mount-point vec)
412 (tramp-get-method-parameter vec 'tramp-mount-args)) 412 (tramp-get-method-parameter vec 'tramp-mount-args))
413 (while (not (file-exists-p (tramp-make-tramp-file-name vec 'noloc))) 413 (while (not (file-exists-p (tramp-make-tramp-file-name vec 'noloc)))
414 (tramp-cleanup-connection vec 'keep-debug 'keep-password)) 414 (tramp-cleanup-connection vec 'keep-debug 'keep-password))
415 (add-to-list 'tramp-fuse-mount-points (tramp-file-name-unify vec))) 415 (add-to-list 'tramp-fuse-mount-points (tramp-file-name-unify vec)))
416 416
417 ;; Mark it as connected. 417 ;; Mark it as connected.
418 (tramp-set-connection-property 418 (tramp-set-connection-property
419 (tramp-get-connection-process vec) "connected" t))) 419 (tramp-get-connection-process vec) "connected" t)))
420 420
421 ;; In `tramp-check-cached-permissions', the connection properties 421 ;; In `tramp-check-cached-permissions', the connection properties
422 ;; "{uid,gid}-{integer,string}" are used. We set them to proper values. 422 ;; "{uid,gid}-{integer,string}" are used. We set them to proper values.
423 (with-tramp-connection-property 423 (with-tramp-connection-property
424 vec "uid-integer" (tramp-get-local-uid 'integer)) 424 vec "uid-integer" (tramp-get-local-uid 'integer))
425 (with-tramp-connection-property 425 (with-tramp-connection-property
426 vec "gid-integer" (tramp-get-local-gid 'integer)) 426 vec "gid-integer" (tramp-get-local-gid 'integer))
427 (with-tramp-connection-property 427 (with-tramp-connection-property
428 vec "uid-string" (tramp-get-local-uid 'string)) 428 vec "uid-string" (tramp-get-local-uid 'string))
429 (with-tramp-connection-property 429 (with-tramp-connection-property
430 vec "gid-string" (tramp-get-local-gid 'string)))) 430 vec "gid-string" (tramp-get-local-gid 'string)))
431 431
432(defun tramp-rclone-send-command (vec &rest args) 432(defun tramp-rclone-send-command (vec &rest args)
433 "Send a command to connection VEC. 433 "Send a command to connection VEC.
diff --git a/lisp/net/tramp-sh.el b/lisp/net/tramp-sh.el
index 97b72ba00ad..13e886b2c13 100644
--- a/lisp/net/tramp-sh.el
+++ b/lisp/net/tramp-sh.el
@@ -1969,7 +1969,7 @@ ID-FORMAT valid values are `string' and `integer'."
1969 (tramp-send-command-and-read 1969 (tramp-send-command-and-read
1970 vec (format "tramp_perl_directory_files_and_attributes %s" 1970 vec (format "tramp_perl_directory_files_and_attributes %s"
1971 (tramp-shell-quote-argument localname))))) 1971 (tramp-shell-quote-argument localname)))))
1972 (when (stringp object) (tramp-error vec 'file-error object)) 1972 (when (stringp object) (tramp-error vec 'remote-file-error object))
1973 object)) 1973 object))
1974 1974
1975;; FIXME: Fix function to work with count parameter. 1975;; FIXME: Fix function to work with count parameter.
@@ -2378,7 +2378,7 @@ the uid and gid from FILENAME."
2378 ((eq op 'copy) "cp -f") 2378 ((eq op 'copy) "cp -f")
2379 ((eq op 'rename) "mv -f") 2379 ((eq op 'rename) "mv -f")
2380 (t (tramp-error 2380 (t (tramp-error
2381 v 'file-error 2381 v 'remote-file-error
2382 "Unknown operation `%s', must be `copy' or `rename'" 2382 "Unknown operation `%s', must be `copy' or `rename'"
2383 op)))) 2383 op))))
2384 (localname1 (tramp-file-local-name filename)) 2384 (localname1 (tramp-file-local-name filename))
@@ -2608,7 +2608,7 @@ The method used must be an out-of-band method."
2608 ;; Check for local copy program. 2608 ;; Check for local copy program.
2609 (unless (executable-find copy-program) 2609 (unless (executable-find copy-program)
2610 (tramp-error 2610 (tramp-error
2611 v 'file-error "Cannot find local copy program: %s" copy-program)) 2611 v 'remote-file-error "Cannot find local copy program: %s" copy-program))
2612 2612
2613 ;; Install listener on the remote side. The prompt must be 2613 ;; Install listener on the remote side. The prompt must be
2614 ;; consumed later on, when the process does not listen anymore. 2614 ;; consumed later on, when the process does not listen anymore.
@@ -2618,7 +2618,7 @@ The method used must be an out-of-band method."
2618 (tramp-find-executable 2618 (tramp-find-executable
2619 v remote-copy-program (tramp-get-remote-path v))) 2619 v remote-copy-program (tramp-get-remote-path v)))
2620 (tramp-error 2620 (tramp-error
2621 v 'file-error 2621 v 'remote-file-error
2622 "Cannot find remote listener: %s" remote-copy-program)) 2622 "Cannot find remote listener: %s" remote-copy-program))
2623 (setq remote-copy-program 2623 (setq remote-copy-program
2624 (string-join 2624 (string-join
@@ -2629,7 +2629,7 @@ The method used must be an out-of-band method."
2629 (tramp-send-command v remote-copy-program) 2629 (tramp-send-command v remote-copy-program)
2630 (with-timeout 2630 (with-timeout
2631 (60 (tramp-error 2631 (60 (tramp-error
2632 v 'file-error 2632 v 'remote-file-error
2633 "Listener process not running on remote host: `%s'" 2633 "Listener process not running on remote host: `%s'"
2634 remote-copy-program)) 2634 remote-copy-program))
2635 (tramp-send-command v (format "netstat -l | grep -q :%s" listener)) 2635 (tramp-send-command v (format "netstat -l | grep -q :%s" listener))
@@ -3468,7 +3468,8 @@ will be used."
3468 3468
3469 ;; Oops, I don't know what to do. 3469 ;; Oops, I don't know what to do.
3470 (t (tramp-error 3470 (t (tramp-error
3471 v 'file-error "Wrong method specification for `%s'" method))) 3471 v 'remote-file-error
3472 "Wrong method specification for `%s'" method)))
3472 3473
3473 ;; Error handling. 3474 ;; Error handling.
3474 ((error quit) 3475 ((error quit)
@@ -3663,7 +3664,7 @@ will be used."
3663 ;; That's not expected. 3664 ;; That's not expected.
3664 (t 3665 (t
3665 (tramp-error 3666 (tramp-error
3666 v 'file-error 3667 v 'remote-file-error
3667 (concat "Method `%s' should specify both encoding and " 3668 (concat "Method `%s' should specify both encoding and "
3668 "decoding command or an scp program") 3669 "decoding command or an scp program")
3669 method))))))))) 3670 method)))))))))
@@ -3689,7 +3690,7 @@ are \"file-exists-p\", \"file-readable-p\", \"file-directory-p\" and
3689 tramp-end-of-heredoc 3690 tramp-end-of-heredoc
3690 (mapconcat #'tramp-shell-quote-argument files "\n") 3691 (mapconcat #'tramp-shell-quote-argument files "\n")
3691 tramp-end-of-heredoc)) 3692 tramp-end-of-heredoc))
3692 (tramp-error vec 'file-error "%s" (tramp-get-buffer-string))) 3693 (tramp-error vec 'remote-file-error "%s" (tramp-get-buffer-string)))
3693 ;; Read the expression. 3694 ;; Read the expression.
3694 (goto-char (point-min)) 3695 (goto-char (point-min))
3695 (read (current-buffer)))) 3696 (read (current-buffer))))
@@ -4165,7 +4166,7 @@ Only send the definition if it has not already been done."
4165 ;; Expand format specifiers. 4166 ;; Expand format specifiers.
4166 (unless (setq script (tramp-expand-script vec script)) 4167 (unless (setq script (tramp-expand-script vec script))
4167 (tramp-error 4168 (tramp-error
4168 vec 'file-error 4169 vec 'remote-file-error
4169 (format "Script %s is not applicable on remote host" name))) 4170 (format "Script %s is not applicable on remote host" name)))
4170 ;; Send it. 4171 ;; Send it.
4171 (tramp-barf-unless-okay 4172 (tramp-barf-unless-okay
@@ -4325,13 +4326,15 @@ file exists and nonzero exit status otherwise."
4325 ;; We cannot use `tramp-get-ls-command', this results in an infloop. 4326 ;; We cannot use `tramp-get-ls-command', this results in an infloop.
4326 ;; (Bug#65321) 4327 ;; (Bug#65321)
4327 (ignore-errors 4328 (ignore-errors
4328 (and (setq result (format "ls -d >%s" (tramp-get-remote-null-device vec))) 4329 (and (setq
4330 result
4331 (format "ls -d >%s" (tramp-get-remote-null-device vec)))
4329 (tramp-send-command-and-check 4332 (tramp-send-command-and-check
4330 vec (format "%s %s" result existing)) 4333 vec (format "%s %s" result existing))
4331 (not (tramp-send-command-and-check 4334 (not (tramp-send-command-and-check
4332 vec (format "%s %s" result nonexistent)))))) 4335 vec (format "%s %s" result nonexistent))))))
4333 (tramp-error 4336 (tramp-error
4334 vec 'file-error "Couldn't find command to check if file exists")) 4337 vec 'remote-file-error "Couldn't find command to check if file exists"))
4335 (tramp-set-file-property vec existing "file-exists-p" t) 4338 (tramp-set-file-property vec existing "file-exists-p" t)
4336 result)) 4339 result))
4337 4340
@@ -4484,7 +4487,8 @@ seconds. If not, it produces an error message with the given ERROR-ARGS."
4484 (error 4487 (error
4485 (delete-process proc) 4488 (delete-process proc)
4486 (apply #'tramp-error-with-buffer 4489 (apply #'tramp-error-with-buffer
4487 (tramp-get-connection-buffer vec) vec 'file-error error-args))))) 4490 (tramp-get-connection-buffer vec) vec
4491 'remote-file-error error-args)))))
4488 4492
4489(defvar tramp-config-check nil 4493(defvar tramp-config-check nil
4490 "A function to be called with one argument, VEC. 4494 "A function to be called with one argument, VEC.
@@ -5293,8 +5297,8 @@ connection if a previous connection has died for some reason."
5293 (unless (and (process-live-p p) 5297 (unless (and (process-live-p p)
5294 (tramp-wait-for-output p 10)) 5298 (tramp-wait-for-output p 10))
5295 ;; The error will be caught locally. 5299 ;; The error will be caught locally.
5296 (tramp-error vec 'file-error "Awake did fail"))) 5300 (tramp-error vec 'remote-file-error "Awake did fail")))
5297 (file-error 5301 (remote-file-error
5298 (tramp-cleanup-connection vec t) 5302 (tramp-cleanup-connection vec t)
5299 (setq p nil))) 5303 (setq p nil)))
5300 5304
@@ -5314,7 +5318,8 @@ connection if a previous connection has died for some reason."
5314 (setenv "HISTFILESIZE" "0") 5318 (setenv "HISTFILESIZE" "0")
5315 (setenv "HISTSIZE" "0")))) 5319 (setenv "HISTSIZE" "0"))))
5316 (unless (stringp tramp-encoding-shell) 5320 (unless (stringp tramp-encoding-shell)
5317 (tramp-error vec 'file-error "`tramp-encoding-shell' not set")) 5321 (tramp-error
5322 vec 'remote-file-error "`tramp-encoding-shell' not set"))
5318 (let* ((current-host tramp-system-name) 5323 (let* ((current-host tramp-system-name)
5319 (target-alist (tramp-compute-multi-hops vec)) 5324 (target-alist (tramp-compute-multi-hops vec))
5320 (previous-hop tramp-null-hop) 5325 (previous-hop tramp-null-hop)
@@ -5520,7 +5525,8 @@ function waits for output unless NOOUTPUT is set."
5520 "Wait for output from remote command." 5525 "Wait for output from remote command."
5521 (unless (buffer-live-p (process-buffer proc)) 5526 (unless (buffer-live-p (process-buffer proc))
5522 (delete-process proc) 5527 (delete-process proc)
5523 (tramp-error proc 'file-error "Process `%s' not available, try again" proc)) 5528 (tramp-error
5529 proc 'remote-file-error "Process `%s' not available, try again" proc))
5524 (with-current-buffer (process-buffer proc) 5530 (with-current-buffer (process-buffer proc)
5525 (let* (;; Initially, `tramp-end-of-output' is "#$ ". There might 5531 (let* (;; Initially, `tramp-end-of-output' is "#$ ". There might
5526 ;; be leading ANSI control escape sequences, which must be 5532 ;; be leading ANSI control escape sequences, which must be
@@ -5551,11 +5557,11 @@ function waits for output unless NOOUTPUT is set."
5551 (delete-region (point) (point-max)))) 5557 (delete-region (point) (point-max))))
5552 (if timeout 5558 (if timeout
5553 (tramp-error 5559 (tramp-error
5554 proc 'file-error 5560 proc 'remote-file-error
5555 "[[Remote prompt `%s' not found in %d secs]]" 5561 "[[Remote prompt `%s' not found in %d secs]]"
5556 tramp-end-of-output timeout) 5562 tramp-end-of-output timeout)
5557 (tramp-error 5563 (tramp-error
5558 proc 'file-error 5564 proc 'remote-file-error
5559 "[[Remote prompt `%s' not found]]" tramp-end-of-output))) 5565 "[[Remote prompt `%s' not found]]" tramp-end-of-output)))
5560 ;; Return value is whether end-of-output sentinel was found. 5566 ;; Return value is whether end-of-output sentinel was found.
5561 found))) 5567 found)))
@@ -5594,7 +5600,7 @@ the exit status."
5594 (with-current-buffer (tramp-get-connection-buffer vec) 5600 (with-current-buffer (tramp-get-connection-buffer vec)
5595 (unless (tramp-search-regexp (rx "tramp_exit_status " (+ digit))) 5601 (unless (tramp-search-regexp (rx "tramp_exit_status " (+ digit)))
5596 (tramp-error 5602 (tramp-error
5597 vec 'file-error "Couldn't find exit status of `%s'" command)) 5603 vec 'remote-file-error "Couldn't find exit status of `%s'" command))
5598 (skip-chars-forward "^ ") 5604 (skip-chars-forward "^ ")
5599 (prog1 5605 (prog1
5600 (if exit-status 5606 (if exit-status
@@ -5608,7 +5614,7 @@ the exit status."
5608Similar to `tramp-send-command-and-check' but accepts two more arguments 5614Similar to `tramp-send-command-and-check' but accepts two more arguments
5609FMT and ARGS which are passed to `error'." 5615FMT and ARGS which are passed to `error'."
5610 (or (tramp-send-command-and-check vec command) 5616 (or (tramp-send-command-and-check vec command)
5611 (apply #'tramp-error vec 'file-error fmt args))) 5617 (apply #'tramp-error vec 'remote-file-error fmt args)))
5612 5618
5613(defun tramp-send-command-and-read (vec command &optional noerror marker) 5619(defun tramp-send-command-and-read (vec command &optional noerror marker)
5614 "Run COMMAND and return the output, which must be a Lisp expression. 5620 "Run COMMAND and return the output, which must be a Lisp expression.
@@ -5627,7 +5633,7 @@ raises an error."
5627 (search-forward-regexp marker) 5633 (search-forward-regexp marker)
5628 (error (unless noerror 5634 (error (unless noerror
5629 (tramp-error 5635 (tramp-error
5630 vec 'file-error 5636 vec 'remote-file-error
5631 "`%s' does not return the marker `%s': `%s'" 5637 "`%s' does not return the marker `%s': `%s'"
5632 command marker (buffer-string)))))) 5638 command marker (buffer-string))))))
5633 ;; Read the expression. 5639 ;; Read the expression.
@@ -5641,7 +5647,7 @@ raises an error."
5641 (error nil))) 5647 (error nil)))
5642 (error (unless noerror 5648 (error (unless noerror
5643 (tramp-error 5649 (tramp-error
5644 vec 'file-error 5650 vec 'remote-file-error
5645 "`%s' does not return a valid Lisp expression: `%s'" 5651 "`%s' does not return a valid Lisp expression: `%s'"
5646 command (buffer-string)))))))) 5652 command (buffer-string))))))))
5647 5653
@@ -5854,7 +5860,8 @@ Nonexistent directories are removed from spec."
5854 (setq result (concat result " --color=never"))) 5860 (setq result (concat result " --color=never")))
5855 (throw 'ls-found result)) 5861 (throw 'ls-found result))
5856 (setq dl (cdr dl)))))) 5862 (setq dl (cdr dl))))))
5857 (tramp-error vec 'file-error "Couldn't find a proper `ls' command")))) 5863 (tramp-error
5864 vec 'remote-file-error "Couldn't find a proper `ls' command"))))
5858 5865
5859(defun tramp-get-ls-command-with (vec option) 5866(defun tramp-get-ls-command-with (vec option)
5860 "Return OPTION, if the remote `ls' command supports the OPTION option." 5867 "Return OPTION, if the remote `ls' command supports the OPTION option."
diff --git a/lisp/net/tramp-smb.el b/lisp/net/tramp-smb.el
index b87eee0fcce..554aa354c00 100644
--- a/lisp/net/tramp-smb.el
+++ b/lisp/net/tramp-smb.el
@@ -821,7 +821,7 @@ PRESERVE-UID-GID and PRESERVE-EXTENDED-ATTRIBUTES are completely ignored."
821 (setq filename (directory-file-name (expand-file-name filename))) 821 (setq filename (directory-file-name (expand-file-name filename)))
822 (with-parsed-tramp-file-name filename nil 822 (with-parsed-tramp-file-name filename nil
823 (tramp-convert-file-attributes v localname id-format 823 (tramp-convert-file-attributes v localname id-format
824 (ignore-errors 824 (condition-case err
825 (if (tramp-smb-get-stat-capability v) 825 (if (tramp-smb-get-stat-capability v)
826 (tramp-smb-do-file-attributes-with-stat v) 826 (tramp-smb-do-file-attributes-with-stat v)
827 ;; Reading just the filename entry via "dir localname" is 827 ;; Reading just the filename entry via "dir localname" is
@@ -851,7 +851,9 @@ PRESERVE-UID-GID and PRESERVE-EXTENDED-ATTRIBUTES are completely ignored."
851 (nth 1 entry) ;8 mode 851 (nth 1 entry) ;8 mode
852 nil ;9 gid weird 852 nil ;9 gid weird
853 inode ;10 inode number 853 inode ;10 inode number
854 device)))))))) ;11 file system number 854 device)))) ;11 file system number
855 (remote-file-error (signal (car err) (cdr err)))
856 (error)))))
855 857
856(defun tramp-smb-do-file-attributes-with-stat (vec) 858(defun tramp-smb-do-file-attributes-with-stat (vec)
857 "Implement `file-attributes' for Tramp files using `stat' command." 859 "Implement `file-attributes' for Tramp files using `stat' command."
@@ -1382,7 +1384,7 @@ will be used."
1382 "Like `make-symbolic-link' for Tramp files." 1384 "Like `make-symbolic-link' for Tramp files."
1383 (let ((v (tramp-dissect-file-name (expand-file-name linkname)))) 1385 (let ((v (tramp-dissect-file-name (expand-file-name linkname))))
1384 (unless (tramp-smb-get-cifs-capabilities v) 1386 (unless (tramp-smb-get-cifs-capabilities v)
1385 (tramp-error v 'file-error "make-symbolic-link not supported"))) 1387 (tramp-error v 'remote-file-error "make-symbolic-link not supported")))
1386 1388
1387 (tramp-skeleton-make-symbolic-link target linkname ok-if-already-exists 1389 (tramp-skeleton-make-symbolic-link target linkname ok-if-already-exists
1388 (unless (tramp-smb-send-command 1390 (unless (tramp-smb-send-command
@@ -1571,8 +1573,7 @@ will be used."
1571 (tramp-search-regexp (rx "tramp_exit_status " (+ digit))) 1573 (tramp-search-regexp (rx "tramp_exit_status " (+ digit)))
1572 (tramp-error 1574 (tramp-error
1573 v 'file-error 1575 v 'file-error
1574 "Couldn't find exit status of `%s'" 1576 "Couldn't find exit status of `%s'" tramp-smb-acl-program))
1575 tramp-smb-acl-program))
1576 (skip-chars-forward "^ ") 1577 (skip-chars-forward "^ ")
1577 (when (zerop (read (current-buffer))) 1578 (when (zerop (read (current-buffer)))
1578 ;; Success. 1579 ;; Success.
@@ -1705,7 +1706,7 @@ If VEC has no cifs capabilities, exchange \"/\" by \"\\\\\"."
1705 (when (string-match-p (rx blank eol) localname) 1706 (when (string-match-p (rx blank eol) localname)
1706 (tramp-error 1707 (tramp-error
1707 vec 'file-error 1708 vec 'file-error
1708 "Invalid file name %s" (tramp-make-tramp-file-name vec localname))) 1709 "Invalid file name `%s'" (tramp-make-tramp-file-name vec localname)))
1709 1710
1710 localname))) 1711 localname)))
1711 1712
@@ -1988,7 +1989,7 @@ If ARGUMENT is non-nil, use it as argument for
1988 (unless tramp-smb-version 1989 (unless tramp-smb-version
1989 (unless (executable-find tramp-smb-program) 1990 (unless (executable-find tramp-smb-program)
1990 (tramp-error 1991 (tramp-error
1991 vec 'file-error 1992 vec 'remote-file-error
1992 "Cannot find command %s in %s" tramp-smb-program exec-path)) 1993 "Cannot find command %s in %s" tramp-smb-program exec-path))
1993 (setq tramp-smb-version (shell-command-to-string command)) 1994 (setq tramp-smb-version (shell-command-to-string command))
1994 (tramp-message vec 6 command) 1995 (tramp-message vec 6 command)
@@ -2165,11 +2166,12 @@ Removes smb prompt. Returns nil if an error message has appeared."
2165 ;; Check for program. 2166 ;; Check for program.
2166 (unless (executable-find tramp-smb-winexe-program) 2167 (unless (executable-find tramp-smb-winexe-program)
2167 (tramp-error 2168 (tramp-error
2168 vec 'file-error "Cannot find program: %s" tramp-smb-winexe-program)) 2169 vec 'remote-file-error "Cannot find program: %s" tramp-smb-winexe-program))
2169 2170
2170 ;; winexe does not supports ports. 2171 ;; winexe does not supports ports.
2171 (when (tramp-file-name-port vec) 2172 (when (tramp-file-name-port vec)
2172 (tramp-error vec 'file-error "Port not supported for remote processes")) 2173 (tramp-error
2174 vec 'remote-file-error "Port not supported for remote processes"))
2173 2175
2174 ;; Check share. 2176 ;; Check share.
2175 (unless (tramp-smb-get-share vec) 2177 (unless (tramp-smb-get-share vec)
diff --git a/lisp/net/tramp-sshfs.el b/lisp/net/tramp-sshfs.el
index 338d128cc4e..2cb5b5b1ed1 100644
--- a/lisp/net/tramp-sshfs.el
+++ b/lisp/net/tramp-sshfs.el
@@ -359,7 +359,7 @@ connection if a previous connection has died for some reason."
359 vec 'tramp-mount-args nil 359 vec 'tramp-mount-args nil
360 ?p (or (tramp-file-name-port vec) "")))))) 360 ?p (or (tramp-file-name-port vec) ""))))))
361 (tramp-error 361 (tramp-error
362 vec 'file-error "Error mounting %s" (tramp-fuse-mount-spec vec))) 362 vec 'remote-file-error "Error mounting %s" (tramp-fuse-mount-spec vec)))
363 363
364 ;; Mark it as connected. 364 ;; Mark it as connected.
365 (add-to-list 'tramp-fuse-mount-points (tramp-file-name-unify vec)) 365 (add-to-list 'tramp-fuse-mount-points (tramp-file-name-unify vec))
diff --git a/lisp/net/tramp-sudoedit.el b/lisp/net/tramp-sudoedit.el
index d3bb8b8478e..9511c899b2b 100644
--- a/lisp/net/tramp-sudoedit.el
+++ b/lisp/net/tramp-sudoedit.el
@@ -52,6 +52,10 @@
52 `(,(rx bos (literal tramp-sudoedit-method) eos) 52 `(,(rx bos (literal tramp-sudoedit-method) eos)
53 nil ,tramp-root-id-string)) 53 nil ,tramp-root-id-string))
54 54
55 (add-to-list 'tramp-default-host-alist
56 `(,(rx bos (literal tramp-sudoedit-method) eos)
57 nil ,(system-name)))
58
55 (tramp-set-completion-function 59 (tramp-set-completion-function
56 tramp-sudoedit-method tramp-completion-function-alist-su)) 60 tramp-sudoedit-method tramp-completion-function-alist-su))
57 61
@@ -742,6 +746,10 @@ connection if a previous connection has died for some reason."
742 (unless (tramp-connectable-p vec) 746 (unless (tramp-connectable-p vec)
743 (throw 'non-essential 'non-essential)) 747 (throw 'non-essential 'non-essential))
744 748
749 (unless (string-match-p tramp-local-host-regexp (tramp-file-name-host vec))
750 (tramp-error
751 vec 'remote-file-error "%s is not a local host" (tramp-file-name-host vec)))
752
745 (with-tramp-debug-message vec "Opening connection" 753 (with-tramp-debug-message vec "Opening connection"
746 ;; We need a process bound to the connection buffer. Therefore, 754 ;; We need a process bound to the connection buffer. Therefore,
747 ;; we create a dummy process. Maybe there is a better solution? 755 ;; we create a dummy process. Maybe there is a better solution?
@@ -775,7 +783,6 @@ in case of error, t otherwise."
775 (append 783 (append
776 (tramp-expand-args 784 (tramp-expand-args
777 vec 'tramp-sudo-login nil 785 vec 'tramp-sudo-login nil
778 ?h (or (tramp-file-name-host vec) "")
779 ?u (or (tramp-file-name-user vec) "")) 786 ?u (or (tramp-file-name-user vec) ""))
780 (flatten-tree args)))) 787 (flatten-tree args))))
781 ;; We suppress the messages `Waiting for prompts from remote shell'. 788 ;; We suppress the messages `Waiting for prompts from remote shell'.
@@ -817,7 +824,7 @@ In case there is no valid Lisp expression, it raises an error."
817 (when (search-forward-regexp (rx (not blank)) (line-end-position) t) 824 (when (search-forward-regexp (rx (not blank)) (line-end-position) t)
818 (error nil))) 825 (error nil)))
819 (error (tramp-error 826 (error (tramp-error
820 vec 'file-error 827 vec 'remote-file-error
821 "`%s' does not return a valid Lisp expression: `%s'" 828 "`%s' does not return a valid Lisp expression: `%s'"
822 (car args) (buffer-string))))))) 829 (car args) (buffer-string)))))))
823 830
diff --git a/lisp/net/tramp.el b/lisp/net/tramp.el
index f57b572532a..5281d8e4db5 100644
--- a/lisp/net/tramp.el
+++ b/lisp/net/tramp.el
@@ -3931,7 +3931,7 @@ BODY is the backend specific code."
3931 ;; The implementation is not complete yet. 3931 ;; The implementation is not complete yet.
3932 (when (and (numberp ,destination) (zerop ,destination)) 3932 (when (and (numberp ,destination) (zerop ,destination))
3933 (tramp-error 3933 (tramp-error
3934 v 'file-error "Implementation does not handle immediate return")) 3934 v 'remote-file-error "Implementation does not handle immediate return"))
3935 3935
3936 (let (command input tmpinput stderr tmpstderr outbuf ret) 3936 (let (command input tmpinput stderr tmpstderr outbuf ret)
3937 ;; Determine input. 3937 ;; Determine input.
@@ -5239,6 +5239,9 @@ Do not set it manually, it is used buffer-local in `tramp-get-lock-pid'.")
5239 ?u (or (tramp-file-name-user (car target-alist)) "") 5239 ?u (or (tramp-file-name-user (car target-alist)) "")
5240 ?h (or (tramp-file-name-host (car target-alist)) "")))) 5240 ?h (or (tramp-file-name-host (car target-alist)) ""))))
5241 (with-parsed-tramp-file-name proxy l 5241 (with-parsed-tramp-file-name proxy l
5242 (when (member l target-alist)
5243 (tramp-user-error
5244 vec "Cycle proxy definition `%s' in multi-hop" proxy))
5242 ;; Add the hop. 5245 ;; Add the hop.
5243 (push l target-alist) 5246 (push l target-alist)
5244 ;; Start next search. 5247 ;; Start next search.
@@ -5505,7 +5508,7 @@ processes."
5505This is the fallback implementation for backends which do not 5508This is the fallback implementation for backends which do not
5506support symbolic links." 5509support symbolic links."
5507 (tramp-error 5510 (tramp-error
5508 (tramp-dissect-file-name (expand-file-name linkname)) 'file-error 5511 (tramp-dissect-file-name (expand-file-name linkname)) 'remote-file-error
5509 "make-symbolic-link not supported")) 5512 "make-symbolic-link not supported"))
5510 5513
5511(defun tramp-handle-memory-info () 5514(defun tramp-handle-memory-info ()
@@ -6255,7 +6258,7 @@ performed successfully. Any other value means an error."
6255 (tramp-clear-passwd vec) 6258 (tramp-clear-passwd vec)
6256 (delete-process proc) 6259 (delete-process proc)
6257 (tramp-error-with-buffer 6260 (tramp-error-with-buffer
6258 (tramp-get-connection-buffer vec) vec 'file-error 6261 (tramp-get-connection-buffer vec) vec 'remote-file-error
6259 (cond 6262 (cond
6260 ((eq exit 'permission-denied) "Permission denied") 6263 ((eq exit 'permission-denied) "Permission denied")
6261 ((eq exit 'out-of-band-failed) 6264 ((eq exit 'out-of-band-failed)
@@ -6402,7 +6405,7 @@ nil."
6402 (tramp-accept-process-output proc) 6405 (tramp-accept-process-output proc)
6403 (unless (process-live-p proc) 6406 (unless (process-live-p proc)
6404 (tramp-error-with-buffer 6407 (tramp-error-with-buffer
6405 nil proc 'file-error "Process has died")) 6408 nil proc 'remote-file-error "Process has died"))
6406 (setq found (tramp-check-for-regexp proc regexp)))) 6409 (setq found (tramp-check-for-regexp proc regexp))))
6407 ;; The process could have timed out, for example due to session 6410 ;; The process could have timed out, for example due to session
6408 ;; timeout of sudo. The process buffer does not exist any longer then. 6411 ;; timeout of sudo. The process buffer does not exist any longer then.
@@ -6412,9 +6415,10 @@ nil."
6412 (unless found 6415 (unless found
6413 (if timeout 6416 (if timeout
6414 (tramp-error 6417 (tramp-error
6415 proc 'file-error "[[Regexp `%s' not found in %d secs]]" 6418 proc 'remote-file-error "[[Regexp `%s' not found in %d secs]]"
6416 regexp timeout) 6419 regexp timeout)
6417 (tramp-error proc 'file-error "[[Regexp `%s' not found]]" regexp))) 6420 (tramp-error
6421 proc 'remote-file-error "[[Regexp `%s' not found]]" regexp)))
6418 found)) 6422 found))
6419 6423
6420;; It seems that Tru64 Unix does not like it if long strings are sent 6424;; It seems that Tru64 Unix does not like it if long strings are sent
@@ -6431,7 +6435,8 @@ the remote host use line-endings as defined in the variable
6431 (chunksize (tramp-get-connection-property p "chunksize"))) 6435 (chunksize (tramp-get-connection-property p "chunksize")))
6432 (unless p 6436 (unless p
6433 (tramp-error 6437 (tramp-error
6434 vec 'file-error "Can't send string to remote host -- not logged in")) 6438 vec 'remote-file-error
6439 "Can't send string to remote host -- not logged in"))
6435 (tramp-set-connection-property p "last-cmd-time" (current-time)) 6440 (tramp-set-connection-property p "last-cmd-time" (current-time))
6436 (tramp-message vec 10 "%s" string) 6441 (tramp-message vec 10 "%s" string)
6437 (with-current-buffer (tramp-get-connection-buffer vec) 6442 (with-current-buffer (tramp-get-connection-buffer vec)
diff --git a/lisp/pixel-scroll.el b/lisp/pixel-scroll.el
index dbb532f691b..23e63add994 100644
--- a/lisp/pixel-scroll.el
+++ b/lisp/pixel-scroll.el
@@ -90,7 +90,6 @@
90(require 'mwheel) 90(require 'mwheel)
91(require 'subr-x) 91(require 'subr-x)
92(require 'ring) 92(require 'ring)
93(require 'cua-base)
94 93
95(defvar pixel-wait 0 94(defvar pixel-wait 0
96 "Idle time on each step of pixel scroll specified in second. 95 "Idle time on each step of pixel scroll specified in second.
@@ -831,7 +830,13 @@ It is a vector of the form [ VELOCITY TIME SIGN ]."
831 ;; since we want exactly 1 830 ;; since we want exactly 1
832 ;; page to be scrolled. 831 ;; page to be scrolled.
833 nil 1) 832 nil 1)
834 (cua-scroll-up))) 833 (cond
834 ((eobp)
835 (scroll-up)) ; signal error
836 (t
837 (condition-case nil
838 (scroll-up)
839 (end-of-buffer (goto-char (point-max))))))))
835 840
836;;;###autoload 841;;;###autoload
837(defun pixel-scroll-interpolate-up () 842(defun pixel-scroll-interpolate-up ()
@@ -840,7 +845,13 @@ It is a vector of the form [ VELOCITY TIME SIGN ]."
840 (if pixel-scroll-precision-interpolate-page 845 (if pixel-scroll-precision-interpolate-page
841 (pixel-scroll-precision-interpolate (window-text-height nil t) 846 (pixel-scroll-precision-interpolate (window-text-height nil t)
842 nil 1) 847 nil 1)
843 (cua-scroll-down))) 848 (cond
849 ((bobp)
850 (scroll-down)) ; signal error
851 (t
852 (condition-case nil
853 (scroll-down)
854 (beginning-of-buffer (goto-char (point-min))))))))
844 855
845;;;###autoload 856;;;###autoload
846(define-minor-mode pixel-scroll-precision-mode 857(define-minor-mode pixel-scroll-precision-mode
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 80099a26ee8..4752b0100d9 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -282,7 +282,8 @@ automatically)."
282 . ,(eglot-alternatives 282 . ,(eglot-alternatives
283 '(("solargraph" "socket" "--port" :autoport) "ruby-lsp"))) 283 '(("solargraph" "socket" "--port" :autoport) "ruby-lsp")))
284 (haskell-mode 284 (haskell-mode
285 . ("haskell-language-server-wrapper" "--lsp")) 285 . ,(eglot-alternatives
286 '(("haskell-language-server-wrapper" "--lsp") "static-ls")))
286 (elm-mode . ("elm-language-server")) 287 (elm-mode . ("elm-language-server"))
287 (mint-mode . ("mint" "ls")) 288 (mint-mode . ("mint" "ls"))
288 ((kotlin-mode kotlin-ts-mode) . ("kotlin-language-server")) 289 ((kotlin-mode kotlin-ts-mode) . ("kotlin-language-server"))
@@ -308,7 +309,7 @@ automatically)."
308 (racket-mode . ("racket" "-l" "racket-langserver")) 309 (racket-mode . ("racket" "-l" "racket-langserver"))
309 ((latex-mode plain-tex-mode context-mode texinfo-mode bibtex-mode tex-mode) 310 ((latex-mode plain-tex-mode context-mode texinfo-mode bibtex-mode tex-mode)
310 . ,(eglot-alternatives '("digestif" "texlab"))) 311 . ,(eglot-alternatives '("digestif" "texlab")))
311 (erlang-mode . ("erlang_ls" "--transport" "stdio")) 312 (erlang-mode . ("elp" "server"))
312 (wat-mode . ("wat_server")) 313 (wat-mode . ("wat_server"))
313 ((yaml-ts-mode yaml-mode) . ("yaml-language-server" "--stdio")) 314 ((yaml-ts-mode yaml-mode) . ("yaml-language-server" "--stdio"))
314 ((toml-ts-mode conf-toml-mode) . ("tombi" "lsp")) 315 ((toml-ts-mode conf-toml-mode) . ("tombi" "lsp"))
@@ -1438,6 +1439,12 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see."
1438 (maphash (lambda (f s) 1439 (maphash (lambda (f s)
1439 (when (eq s server) (remhash f eglot--servers-by-xrefed-file))) 1440 (when (eq s server) (remhash f eglot--servers-by-xrefed-file)))
1440 eglot--servers-by-xrefed-file) 1441 eglot--servers-by-xrefed-file)
1442 ;; Cleanup entries in 'flymake-list-only-diagnostics'
1443 (setq flymake-list-only-diagnostics
1444 (cl-delete-if
1445 (lambda (x) (eq server
1446 (get-text-property 0 'eglot--server (car x))))
1447 flymake-list-only-diagnostics))
1441 (cond ((eglot--shutdown-requested server) 1448 (cond ((eglot--shutdown-requested server)
1442 t) 1449 t)
1443 ((not (eglot--inhibit-autoreconnect server)) 1450 ((not (eglot--inhibit-autoreconnect server))
@@ -2024,21 +2031,25 @@ according to `eglot-advertise-cancellation'.")
2024 (timeout-fn nil timeout-fn-supplied-p) 2031 (timeout-fn nil timeout-fn-supplied-p)
2025 (timeout nil timeout-supplied-p) 2032 (timeout nil timeout-supplied-p)
2026 hint 2033 hint
2027 &aux moreargs 2034 &aux moreargs id
2028 id (buf (current-buffer))) 2035 (buf (current-buffer))
2036 (inflight eglot--inflight-async-requests))
2029 "Like `jsonrpc-async-request', but for Eglot LSP requests. 2037 "Like `jsonrpc-async-request', but for Eglot LSP requests.
2030SUCCESS-FN, ERROR-FN and TIMEOUT-FN run in buffer of call site. 2038SUCCESS-FN, ERROR-FN and TIMEOUT-FN run in buffer of call site.
2031HINT argument is a symbol passed as DEFERRED to `jsonrpc-async-request' 2039HINT argument is a symbol passed as DEFERRED to `jsonrpc-async-request'
2032and also used as a hint of the request cancellation mechanism (see 2040and also used as a hint of the request cancellation mechanism (see
2033`eglot-advertise-cancellation')." 2041`eglot-advertise-cancellation')."
2034 (cl-labels 2042 (cl-labels
2035 ((clearing-fn (fn) 2043 ((wrapfn (fn)
2036 (lambda (&rest args) 2044 (lambda (&rest args)
2037 (eglot--when-live-buffer buf 2045 (eglot--when-live-buffer buf
2038 (when (and 2046 (cond (eglot-advertise-cancellation
2039 fn (memq id (cl-getf eglot--inflight-async-requests hint))) 2047 (when-let* ((tail (and fn (plist-member inflight hint))))
2040 (apply fn args)) 2048 (when (memq id (cadr tail))
2041 (cl-remf eglot--inflight-async-requests hint))))) 2049 (apply fn args))
2050 (setf (cadr tail) (delete id (cadr tail)))))
2051 (t
2052 (apply fn args)))))))
2042 (eglot--cancel-inflight-async-requests (list hint)) 2053 (eglot--cancel-inflight-async-requests (list hint))
2043 (when timeout-supplied-p 2054 (when timeout-supplied-p
2044 (setq moreargs (nconc `(:timeout ,timeout) moreargs))) 2055 (setq moreargs (nconc `(:timeout ,timeout) moreargs)))
@@ -2047,13 +2058,12 @@ and also used as a hint of the request cancellation mechanism (see
2047 (setq id 2058 (setq id
2048 (car (apply #'jsonrpc-async-request 2059 (car (apply #'jsonrpc-async-request
2049 server method params 2060 server method params
2050 :success-fn (clearing-fn success-fn) 2061 :success-fn (wrapfn success-fn)
2051 :error-fn (clearing-fn error-fn) 2062 :error-fn (wrapfn error-fn)
2052 :timeout-fn (clearing-fn timeout-fn) 2063 :timeout-fn (wrapfn timeout-fn)
2053 moreargs))) 2064 moreargs)))
2054 (when (and hint eglot-advertise-cancellation) 2065 (when (and hint eglot-advertise-cancellation)
2055 (push id 2066 (push id (plist-get inflight hint)))
2056 (plist-get eglot--inflight-async-requests hint)))
2057 id)) 2067 id))
2058 2068
2059(cl-defun eglot--delete-overlays (&optional (prop 'eglot--overlays)) 2069(cl-defun eglot--delete-overlays (&optional (prop 'eglot--overlays))
@@ -3422,11 +3432,8 @@ object. The originator of this \"push\" is usually either regular
3422 (with-current-buffer buffer 3432 (with-current-buffer buffer
3423 (if (and version (/= version eglot--docver)) 3433 (if (and version (/= version eglot--docver))
3424 (cl-return-from eglot--flymake-handle-push)) 3434 (cl-return-from eglot--flymake-handle-push))
3425 (setq 3435 ;; if no explicit version received, assume it's current.
3426 ;; if no explicit version received, assume it's current. 3436 (setq version eglot--docver)
3427 version eglot--docver
3428 flymake-list-only-diagnostics
3429 (assoc-delete-all path flymake-list-only-diagnostics))
3430 (funcall then diagnostics)) 3437 (funcall then diagnostics))
3431 (cl-loop 3438 (cl-loop
3432 for diag-spec across diagnostics 3439 for diag-spec across diagnostics
@@ -3437,12 +3444,13 @@ object. The originator of this \"push\" is usually either regular
3437 (flymake-make-diagnostic 3444 (flymake-make-diagnostic
3438 path (cons line char) nil 3445 path (cons line char) nil
3439 (eglot--flymake-diag-type severity) 3446 (eglot--flymake-diag-type severity)
3440 (list source code message)))) 3447 (list source code message)
3448 `((eglot-lsp-diag . ,diag-spec)))))
3441 into diags 3449 into diags
3442 finally 3450 finally
3443 (setq flymake-list-only-diagnostics 3451 (setf (alist-get (propertize path 'eglot--server server)
3444 (assoc-delete-all path flymake-list-only-diagnostics)) 3452 flymake-list-only-diagnostics nil nil #'equal)
3445 (push (cons path diags) flymake-list-only-diagnostics)))) 3453 diags))))
3446 3454
3447(cl-defun eglot--flymake-pull (&aux (server (eglot--current-server-or-lose)) 3455(cl-defun eglot--flymake-pull (&aux (server (eglot--current-server-or-lose))
3448 (origin (current-buffer))) 3456 (origin (current-buffer)))
@@ -3506,6 +3514,17 @@ MODE is like `eglot--flymake-report-1'."
3506 (pushed-outdated-p (and pushed-docver (< pushed-docver eglot--docver)))) 3514 (pushed-outdated-p (and pushed-docver (< pushed-docver eglot--docver))))
3507 "Push previously collected diagnostics to `eglot--flymake-report-fn'. 3515 "Push previously collected diagnostics to `eglot--flymake-report-fn'.
3508If KEEP, knowingly push a dummy do-nothing update." 3516If KEEP, knowingly push a dummy do-nothing update."
3517 ;; Maybe hack in diagnostics we previously may have saved in
3518 ;; `flymake-list-only-diagnostics', pushed for this file before it was
3519 ;; visited (github#1531).
3520 (when-let* ((hack (and (<= eglot--docver 0)
3521 (null eglot--pushed-diagnostics)
3522 (cdr (assoc (buffer-file-name)
3523 flymake-list-only-diagnostics)))))
3524 (cl-loop
3525 for x in hack
3526 collect (alist-get 'eglot-lsp-diag (flymake-diagnostic-data x)) into res
3527 finally (setq eglot--pushed-diagnostics `(,(vconcat res) ,eglot--docver))))
3509 (eglot--widening 3528 (eglot--widening
3510 (if (and (null eglot--pulled-diagnostics) pushed-outdated-p) 3529 (if (and (null eglot--pulled-diagnostics) pushed-outdated-p)
3511 ;; Here, we don't have anything interesting to give to Flymake. 3530 ;; Here, we don't have anything interesting to give to Flymake.
diff --git a/lisp/progmodes/elisp-mode.el b/lisp/progmodes/elisp-mode.el
index c4fb6946aeb..f5c3dc3fbb2 100644
--- a/lisp/progmodes/elisp-mode.el
+++ b/lisp/progmodes/elisp-mode.el
@@ -1783,7 +1783,9 @@ and `eval-expression-print-level'.
1783 (funcall 1783 (funcall
1784 (syntax-propertize-rules 1784 (syntax-propertize-rules
1785 (emacs-lisp-byte-code-comment-re 1785 (emacs-lisp-byte-code-comment-re
1786 (1 (prog1 "< b" (elisp--byte-code-comment end (point)))))) 1786 (1 (prog1 "< b"
1787 (goto-char (match-end 2))
1788 (elisp--byte-code-comment end (point))))))
1787 start end)) 1789 start end))
1788 1790
1789;;;###autoload 1791;;;###autoload
diff --git a/lisp/progmodes/etags-regen.el b/lisp/progmodes/etags-regen.el
index b6adca8af7a..3d3ddc0521f 100644
--- a/lisp/progmodes/etags-regen.el
+++ b/lisp/progmodes/etags-regen.el
@@ -348,7 +348,7 @@ File extensions to generate the tags for."
348 348
349(defun etags-regen--build-program-options (ctags-p) 349(defun etags-regen--build-program-options (ctags-p)
350 (when (and etags-regen-regexp-alist ctags-p) 350 (when (and etags-regen-regexp-alist ctags-p)
351 (user-error "etags-regen-regexp-alist is not supported with Ctags")) 351 (user-error "etags-regen-regexp-alist not supported with Ctags; to use this option, customize `etags-regen-program'"))
352 (nconc 352 (nconc
353 (mapcan 353 (mapcan
354 (lambda (group) 354 (lambda (group)
diff --git a/lisp/progmodes/etags.el b/lisp/progmodes/etags.el
index 79cfb91caa9..f7532fce6b1 100644
--- a/lisp/progmodes/etags.el
+++ b/lisp/progmodes/etags.el
@@ -2114,8 +2114,14 @@ file name, add `tag-partial-file-name-match-p' to the list value.")
2114 :type 'boolean 2114 :type 'boolean
2115 :version "28.1") 2115 :version "28.1")
2116 2116
2117;;;###autoload 2117;;;###autoload (defun etags--xref-backend ()
2118(defun etags--xref-backend () 'etags) 2118;;;###autoload (when (or tags-table-list tags-file-name)
2119;;;###autoload (load "etags")
2120;;;###autoload 'etags))
2121
2122(defun etags--xref-backend ()
2123 (when (or tags-table-list tags-file-name)
2124 'etags))
2119 2125
2120(cl-defmethod xref-backend-identifier-at-point ((_backend (eql 'etags))) 2126(cl-defmethod xref-backend-identifier-at-point ((_backend (eql 'etags)))
2121 (find-tag--default)) 2127 (find-tag--default))
diff --git a/lisp/progmodes/make-mode.el b/lisp/progmodes/make-mode.el
index 8856856100e..e34eaba3150 100644
--- a/lisp/progmodes/make-mode.el
+++ b/lisp/progmodes/make-mode.el
@@ -331,7 +331,7 @@ not be enclosed in { } or ( )."
331 &rest fl-keywords) 331 &rest fl-keywords)
332 `(;; Do macro assignments. These get the "variable-name" face. 332 `(;; Do macro assignments. These get the "variable-name" face.
333 (,makefile-macroassign-regex 333 (,makefile-macroassign-regex
334 (1 font-lock-variable-name-face) 334 (1 'font-lock-variable-name-face)
335 ;; This is for after != 335 ;; This is for after !=
336 (2 'makefile-shell prepend t) 336 (2 'makefile-shell prepend t)
337 ;; This is for after normal assignment 337 ;; This is for after normal assignment
@@ -340,10 +340,10 @@ not be enclosed in { } or ( )."
340 ;; Rule actions. 340 ;; Rule actions.
341 ;; FIXME: When this spans multiple lines we need font-lock-multiline. 341 ;; FIXME: When this spans multiple lines we need font-lock-multiline.
342 (makefile-match-action 342 (makefile-match-action
343 (1 font-lock-type-face nil t) 343 (1 'font-lock-type-face nil t)
344 (2 'makefile-shell prepend) 344 (2 'makefile-shell prepend)
345 ;; Only makepp has builtin commands. 345 ;; Only makepp has builtin commands.
346 (3 font-lock-builtin-face prepend t)) 346 (3 'font-lock-builtin-face prepend t))
347 347
348 ;; Variable references even in targets/strings/comments. 348 ;; Variable references even in targets/strings/comments.
349 (,var 2 font-lock-variable-name-face prepend) 349 (,var 2 font-lock-variable-name-face prepend)
@@ -364,11 +364,11 @@ not be enclosed in { } or ( )."
364 (string-replace "-" "[_-]" (regexp-opt (cdr keywords) t)) 364 (string-replace "-" "[_-]" (regexp-opt (cdr keywords) t))
365 (regexp-opt keywords t))) 365 (regexp-opt keywords t)))
366 "\\>[ \t]*\\([^: \t\n#]*\\)") 366 "\\>[ \t]*\\([^: \t\n#]*\\)")
367 (1 font-lock-keyword-face) (2 font-lock-variable-name-face)))) 367 (1 'font-lock-keyword-face) (2 'font-lock-variable-name-face))))
368 368
369 ,@(if negation 369 ,@(if negation
370 `((,negation (1 font-lock-negation-char-face prepend) 370 `((,negation (1 'font-lock-negation-char-face prepend)
371 (2 font-lock-negation-char-face prepend t)))) 371 (2 'font-lock-negation-char-face prepend t))))
372 372
373 ,@(if space 373 ,@(if space
374 '(;; Highlight lines that contain just whitespace. 374 '(;; Highlight lines that contain just whitespace.
@@ -436,9 +436,9 @@ not be enclosed in { } or ( )."
436 436
437 ;; Colon modifier keywords. 437 ;; Colon modifier keywords.
438 '("\\(:\\s *\\)\\(build_c\\(?:ache\\|heck\\)\\|env\\(?:ironment\\)?\\|foreach\\|signature\\|scanner\\|quickscan\\|smartscan\\)\\>\\([^:\n]*\\)" 438 '("\\(:\\s *\\)\\(build_c\\(?:ache\\|heck\\)\\|env\\(?:ironment\\)?\\|foreach\\|signature\\|scanner\\|quickscan\\|smartscan\\)\\>\\([^:\n]*\\)"
439 (1 font-lock-type-face t) 439 (1 'font-lock-type-face t)
440 (2 font-lock-keyword-face t) 440 (2 'font-lock-keyword-face t)
441 (3 font-lock-variable-name-face t)) 441 (3 'font-lock-variable-name-face t))
442 442
443 ;; $(function ...) $((function ...)) ${...} ${{...}} $[...] $[[...]] 443 ;; $(function ...) $((function ...)) ${...} ${{...}} $[...] $[[...]]
444 '("[^$]\\$\\(?:((?\\|{{?\\|\\[\\[?\\)\\([-a-zA-Z0-9_.]+\\s \\)" 444 '("[^$]\\$\\(?:((?\\|{{?\\|\\[\\[?\\)\\([-a-zA-Z0-9_.]+\\s \\)"
diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index 997c876b1fa..9e5a8be5e13 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -84,6 +84,12 @@
84;; This project type can also be used for non-VCS controlled 84;; This project type can also be used for non-VCS controlled
85;; directories, see the variable `project-vc-extra-root-markers'. 85;; directories, see the variable `project-vc-extra-root-markers'.
86;; 86;;
87;; Some of the methods on this backend cache their computations for time
88;; determined either by variable `project-vc-cache-timeout' or
89;; `project-vc-non-essential-cache-timeout', depending on whether the
90;; MAYBE-PROMPT argument to `project-current' is non-nil, or the value
91;; of `non-essential' when project methods are called.
92;;
87;; Utils: 93;; Utils:
88;; 94;;
89;; `project-combine-directories' and `project-subtract-directories', 95;; `project-combine-directories' and `project-subtract-directories',
@@ -275,7 +281,8 @@ of the project instance object."
275 (if pr 281 (if pr
276 (project-remember-project pr) 282 (project-remember-project pr)
277 (project--remove-from-project-list 283 (project--remove-from-project-list
278 directory "Project `%s' not found; removed from list") 284 (abbreviate-file-name directory)
285 "Project `%s' not found; removed from list")
279 (setq pr (cons 'transient directory)))) 286 (setq pr (cons 'transient directory))))
280 pr)) 287 pr))
281 288
@@ -586,16 +593,74 @@ project backend implementation of `project-external-roots'.")
586 593
587See `project-vc-extra-root-markers' for the marker value format.") 594See `project-vc-extra-root-markers' for the marker value format.")
588 595
589;; FIXME: Should perhaps use `vc--repo-*prop' functions 596(defvar project-vc-cache-timeout '((file-remote-p . nil)
590;; (after promoting those to public). --spwhitton 597 (always . 2))
598 "Number of seconds to cache a value in VC-aware project methods.
599It can be nil, a number, or an alist where
600the key is a predicate, and the value is a number.
601A predicate function should take a directory string and if it returns
602non-nil, the corresponding value will be used as the timeout.
603Set to nil to disable time-based expiration.")
604
605(defvar project-vc-non-essential-cache-timeout '((file-remote-p . nil)
606 (always . 300))
607 "Number of seconds to cache non-essential information.
608The format of the value is same as `project-vc-cache-timeout', but while
609the former is intended for interactive commands, this variable uses
610higher numbers, intended for \"background\" things like
611`project-mode-line' indicators and `project-uniquify-dirname-transform'.
612It is used when `non-essential' is non-nil.")
613
614(defun project--get-cached (dir key)
615 (let ((cached (vc-file-getprop dir key))
616 (current-time (float-time)))
617 (when (and (numberp (cdr cached))
618 ;; Support package upgrade mid-session.
619 (let* ((project-vc-cache-timeout
620 (if non-essential
621 project-vc-non-essential-cache-timeout
622 project-vc-cache-timeout))
623 (timeout
624 (cond
625 ((numberp project-vc-cache-timeout)
626 project-vc-cache-timeout)
627 ((null project-vc-cache-timeout)
628 nil)
629 ((listp project-vc-cache-timeout)
630 (cdr
631 (seq-find (lambda (pair)
632 (and (functionp (car pair))
633 (funcall (car pair) dir)))
634 project-vc-cache-timeout)))
635 (t nil))))
636 (or (null timeout)
637 (< (- current-time (cdr cached)) timeout))))
638 (car cached))))
639
640(defun project--set-cached (dir key value)
641 (vc-file-setprop dir key (cons value (float-time))))
642
643;; TODO: We can have our own, separate obarray.
644(defun project--clear-cache ()
645 (obarray-map
646 (lambda (sym)
647 (if (get sym 'project-vc)
648 (put sym 'project-vc nil)))
649 vc-file-prop-obarray))
650
591(defun project-try-vc (dir) 651(defun project-try-vc (dir)
592 ;; FIXME: Learn to invalidate when the value changes: 652 "Returns a project value corresponding to DIR from the VC-aware backend.
593 ;; `project-vc-merge-submodules' or `project-vc-extra-root-markers'. 653
594 (or (vc-file-getprop dir 'project-vc) 654The value is cached, and depending on whether MAYBE-PROMPT was non-nil
595 ;; FIXME: Cache for a shorter time (bug#78545). 655in the `project-current' call, the timeout is determined by
596 (let ((res (project-try-vc--search dir))) 656`project-vc-cache-timeout' or `project-vc-non-essential-cache-timeout'."
597 (and res (vc-file-setprop dir 'project-vc res)) 657 (let ((cached (project--get-cached dir 'project-vc)))
598 res))) 658 (if (eq cached 'none)
659 nil
660 (or cached
661 (let ((res (project-try-vc--search dir)))
662 (project--set-cached dir 'project-vc (or res 'none))
663 res)))))
599 664
600(defun project-try-vc--search (dir) 665(defun project-try-vc--search (dir)
601 (let* ((backend-markers 666 (let* ((backend-markers
@@ -896,13 +961,24 @@ DIRS must contain directory names."
896 (cl-set-difference files dirs :test #'file-in-directory-p)) 961 (cl-set-difference files dirs :test #'file-in-directory-p))
897 962
898(defun project--value-in-dir (var dir) 963(defun project--value-in-dir (var dir)
964 (alist-get
965 var
966 (let ((cached (project--get-cached dir 'project-vc-dir-locals)))
967 (if (eq cached 'none)
968 nil
969 (or cached
970 (let ((res (project--read-dir-locals dir)))
971 (project--set-cached dir 'project-vc-dir-locals (or res 'none))
972 res))))
973 (symbol-value var)))
974
975(defun project--read-dir-locals (dir)
899 (with-temp-buffer 976 (with-temp-buffer
900 (setq default-directory (file-name-as-directory dir)) 977 (setq default-directory (file-name-as-directory dir))
978 ;; Don't use `hack-local-variables-apply' to avoid setting modes.
901 (let ((enable-local-variables :all)) 979 (let ((enable-local-variables :all))
902 (hack-dir-local-variables)) 980 (hack-dir-local-variables))
903 ;; Don't use `hack-local-variables-apply' to avoid setting modes. 981 file-local-variables-alist))
904 (alist-get var file-local-variables-alist
905 (symbol-value var))))
906 982
907(cl-defmethod project-buffers ((project (head vc))) 983(cl-defmethod project-buffers ((project (head vc)))
908 (let* ((root (expand-file-name (file-name-as-directory (project-root project)))) 984 (let* ((root (expand-file-name (file-name-as-directory (project-root project))))
@@ -924,6 +1000,11 @@ DIRS must contain directory names."
924 (nreverse bufs))) 1000 (nreverse bufs)))
925 1001
926(cl-defmethod project-name ((project (head vc))) 1002(cl-defmethod project-name ((project (head vc)))
1003 "Returns the name of this VC-aware type PROJECT.
1004
1005The value is cached, and depending on whether `non-essential' is nil,
1006the timeout is determined by `project-vc-cache-timeout' or
1007`project-vc-non-essential-cache-timeout'."
927 (or (project--value-in-dir 'project-vc-name (project-root project)) 1008 (or (project--value-in-dir 'project-vc-name (project-root project))
928 (cl-call-next-method))) 1009 (cl-call-next-method)))
929 1010
@@ -2206,7 +2287,7 @@ result in `project-list-file'. Announce the project's removal
2206from the list using REPORT-MESSAGE, which is a format string 2287from the list using REPORT-MESSAGE, which is a format string
2207passed to `message' as its first argument." 2288passed to `message' as its first argument."
2208 (project--ensure-read-project-list) 2289 (project--ensure-read-project-list)
2209 (when-let* ((ent (assoc (abbreviate-file-name project-root) project--list))) 2290 (when-let* ((ent (assoc project-root project--list)))
2210 (setq project--list (delq ent project--list)) 2291 (setq project--list (delq ent project--list))
2211 (message report-message project-root) 2292 (message report-message project-root)
2212 (project--write-project-list))) 2293 (project--write-project-list)))
@@ -2385,6 +2466,7 @@ projects.
2385Display a message at the end summarizing what was found. 2466Display a message at the end summarizing what was found.
2386Return the number of detected projects." 2467Return the number of detected projects."
2387 (interactive "DDirectory: \nP") 2468 (interactive "DDirectory: \nP")
2469 (project--clear-cache)
2388 (project--ensure-read-project-list) 2470 (project--ensure-read-project-list)
2389 (let ((dirs (if recursive 2471 (let ((dirs (if recursive
2390 (directory-files-recursively dir "" t) 2472 (directory-files-recursively dir "" t)
@@ -2417,12 +2499,18 @@ PREDICATE can be a function with 1 argument which determines which
2417projects should be deleted." 2499projects should be deleted."
2418 (dolist (proj (project-known-project-roots)) 2500 (dolist (proj (project-known-project-roots))
2419 (when (and (funcall (or predicate #'identity) proj) 2501 (when (and (funcall (or predicate #'identity) proj)
2420 (not (file-exists-p proj))) 2502 (condition-case-unless-debug nil
2503 (not (file-exists-p proj))
2504 (file-error
2505 (yes-or-no-p
2506 (format "Forget unreachable project `%s'? "
2507 proj)))))
2421 (project-forget-project proj)))) 2508 (project-forget-project proj))))
2422 2509
2423(defun project-forget-zombie-projects (&optional interactive) 2510(defun project-forget-zombie-projects (&optional interactive)
2424 "Forget all known projects that don't exist any more." 2511 "Forget all known projects that don't exist any more."
2425 (interactive (list t)) 2512 (interactive (list t))
2513 (project--clear-cache)
2426 (let ((pred (when interactive (alist-get 'interactively project-prune-zombie-projects)))) 2514 (let ((pred (when interactive (alist-get 'interactively project-prune-zombie-projects))))
2427 (project--delete-zombie-projects pred))) 2515 (project--delete-zombie-projects pred)))
2428 2516
@@ -2435,6 +2523,7 @@ to remove those projects from the index.
2435Display a message at the end summarizing what was forgotten. 2523Display a message at the end summarizing what was forgotten.
2436Return the number of forgotten projects." 2524Return the number of forgotten projects."
2437 (interactive "DDirectory: \nP") 2525 (interactive "DDirectory: \nP")
2526 (project--clear-cache)
2438 (let ((count 0)) 2527 (let ((count 0))
2439 (if recursive 2528 (if recursive
2440 (dolist (proj (project-known-project-roots)) 2529 (dolist (proj (project-known-project-roots))
@@ -2624,7 +2713,8 @@ slash-separated components from `project-name' will be appended to
2624the buffer's directory name when buffers from two different projects 2713the buffer's directory name when buffers from two different projects
2625would otherwise have the same name." 2714would otherwise have the same name."
2626 (if-let* ((proj (project-current nil dirname))) 2715 (if-let* ((proj (project-current nil dirname)))
2627 (let ((root (project-root proj))) 2716 (let ((root (project-root proj))
2717 (non-essential t))
2628 (expand-file-name 2718 (expand-file-name
2629 (file-name-concat 2719 (file-name-concat
2630 (file-name-directory root) 2720 (file-name-directory root)
@@ -2634,27 +2724,6 @@ would otherwise have the same name."
2634 2724
2635;;; Project mode-line 2725;;; Project mode-line
2636 2726
2637(defvar project-name-cache-timeout 300
2638 "Number of seconds to cache the project name.
2639Used by `project-name-cached'.")
2640
2641(defun project-name-cached (dir)
2642 "Return the cached project name for the directory DIR.
2643Until it's cached, retrieve the project name using `project-current'
2644and `project-name', then put the name to the cache for the time defined
2645by the variable `project-name-cache-timeout'. This function is useful
2646for project indicators such as on the mode line."
2647 (let ((cached (vc-file-getprop dir 'project-name))
2648 (current-time (float-time)))
2649 (if (and cached (< (- current-time (cdr cached))
2650 project-name-cache-timeout))
2651 (let ((value (car cached)))
2652 (if (eq value 'none) nil value))
2653 (let ((res (when-let* ((project (project-current nil dir)))
2654 (project-name project))))
2655 (vc-file-setprop dir 'project-name (cons (or res 'none) current-time))
2656 res))))
2657
2658;;;###autoload 2727;;;###autoload
2659(defcustom project-mode-line nil 2728(defcustom project-mode-line nil
2660 "Whether to show current project name and Project menu on the mode line. 2729 "Whether to show current project name and Project menu on the mode line.
@@ -2691,7 +2760,9 @@ value is `non-remote', show the project name only for local files."
2691 ;; 'last-coding-system-used' when reading the project name 2760 ;; 'last-coding-system-used' when reading the project name
2692 ;; from .dir-locals.el also enables flyspell-mode (bug#66825). 2761 ;; from .dir-locals.el also enables flyspell-mode (bug#66825).
2693 (when-let* ((last-coding-system-used last-coding-system-used) 2762 (when-let* ((last-coding-system-used last-coding-system-used)
2694 (project-name (project-name-cached default-directory))) 2763 (non-essential t)
2764 (project (project-current))
2765 (project-name (project-name project)))
2695 (concat 2766 (concat
2696 " " 2767 " "
2697 (propertize 2768 (propertize
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el
index b6981c9156c..2a3035c95c5 100644
--- a/lisp/progmodes/python.el
+++ b/lisp/progmodes/python.el
@@ -3366,6 +3366,16 @@ from `python-shell-prompt-regexp',
3366 python-shell--prompt-calculated-output-regexp 3366 python-shell--prompt-calculated-output-regexp
3367 (funcall build-regexp output-prompts))))) 3367 (funcall build-regexp output-prompts)))))
3368 3368
3369(defun python-shell-get-project-name ()
3370 "Return the project name for the current buffer.
3371Use `project-name-cached' if available."
3372 (when (featurep 'project)
3373 (if (fboundp 'project-name-cached)
3374 (project-name-cached default-directory)
3375 (when-let* ((proj (project-current)))
3376 (file-name-nondirectory
3377 (directory-file-name (project-root proj)))))))
3378
3369(defun python-shell-get-process-name (dedicated) 3379(defun python-shell-get-process-name (dedicated)
3370 "Calculate the appropriate process name for inferior Python process. 3380 "Calculate the appropriate process name for inferior Python process.
3371If DEDICATED is nil, this is simply `python-shell-buffer-name'. 3381If DEDICATED is nil, this is simply `python-shell-buffer-name'.
@@ -3374,11 +3384,8 @@ name respectively the current project name."
3374 (pcase dedicated 3384 (pcase dedicated
3375 ('nil python-shell-buffer-name) 3385 ('nil python-shell-buffer-name)
3376 ('project 3386 ('project
3377 (if-let* ((proj (and (featurep 'project) 3387 (if-let* ((proj-name (python-shell-get-project-name)))
3378 (project-current)))) 3388 (format "%s[%s]" python-shell-buffer-name proj-name)
3379 (format "%s[%s]" python-shell-buffer-name (file-name-nondirectory
3380 (directory-file-name
3381 (project-root proj))))
3382 python-shell-buffer-name)) 3389 python-shell-buffer-name))
3383 (_ (format "%s[%s]" python-shell-buffer-name (buffer-name))))) 3390 (_ (format "%s[%s]" python-shell-buffer-name (buffer-name)))))
3384 3391
@@ -3816,16 +3823,6 @@ variable.
3816 (compilation-shell-minor-mode 1) 3823 (compilation-shell-minor-mode 1)
3817 (python-pdbtrack-setup-tracking)) 3824 (python-pdbtrack-setup-tracking))
3818 3825
3819(defvar-local python-shell--process-cache)
3820(defvar-local python-shell--process-cache-valid)
3821
3822(defun python-shell--invalidate-process-cache ()
3823 "Invalidate process cache."
3824 (dolist (buffer (buffer-list))
3825 (with-current-buffer buffer
3826 (setq python-shell--process-cache nil
3827 python-shell--process-cache-valid nil))))
3828
3829(defun python-shell-make-comint (cmd proc-name &optional show internal) 3826(defun python-shell-make-comint (cmd proc-name &optional show internal)
3830 "Create a Python shell comint buffer. 3827 "Create a Python shell comint buffer.
3831CMD is the Python command to be executed and PROC-NAME is the 3828CMD is the Python command to be executed and PROC-NAME is the
@@ -3842,7 +3839,6 @@ killed."
3842 (let* ((proc-buffer-name 3839 (let* ((proc-buffer-name
3843 (format (if (not internal) "*%s*" " *%s*") proc-name))) 3840 (format (if (not internal) "*%s*" " *%s*") proc-name)))
3844 (when (not (comint-check-proc proc-buffer-name)) 3841 (when (not (comint-check-proc proc-buffer-name))
3845 (python-shell--invalidate-process-cache)
3846 (let* ((cmdlist (split-string-and-unquote cmd)) 3842 (let* ((cmdlist (split-string-and-unquote cmd))
3847 (interpreter (car cmdlist)) 3843 (interpreter (car cmdlist))
3848 (args (cdr cmdlist)) 3844 (args (cdr cmdlist))
@@ -3966,15 +3962,7 @@ If current buffer is in `inferior-python-mode', return it."
3966 3962
3967(defun python-shell-get-process () 3963(defun python-shell-get-process ()
3968 "Return inferior Python process for current buffer." 3964 "Return inferior Python process for current buffer."
3969 (unless (and python-shell--process-cache-valid 3965 (get-buffer-process (python-shell-get-buffer)))
3970 (or (not python-shell--process-cache)
3971 (and (process-live-p python-shell--process-cache)
3972 (buffer-live-p
3973 (process-buffer python-shell--process-cache)))))
3974 (setq python-shell--process-cache
3975 (get-buffer-process (python-shell-get-buffer))
3976 python-shell--process-cache-valid t))
3977 python-shell--process-cache)
3978 3966
3979(defun python-shell-get-process-or-error (&optional interactivep) 3967(defun python-shell-get-process-or-error (&optional interactivep)
3980 "Return inferior Python process for current buffer or signal error. 3968 "Return inferior Python process for current buffer or signal error.
@@ -5854,7 +5842,7 @@ Set to nil by `python-eldoc-function' if
5854 5842
5855(defcustom python-eldoc-function-timeout 1 5843(defcustom python-eldoc-function-timeout 1
5856 "Timeout for `python-eldoc-function' in seconds." 5844 "Timeout for `python-eldoc-function' in seconds."
5857 :type 'integer 5845 :type 'number
5858 :version "25.1") 5846 :version "25.1")
5859 5847
5860(defcustom python-eldoc-function-timeout-permanent t 5848(defcustom python-eldoc-function-timeout-permanent t
diff --git a/lisp/progmodes/xref.el b/lisp/progmodes/xref.el
index 84a3fa4dfba..1e51b23eaff 100644
--- a/lisp/progmodes/xref.el
+++ b/lisp/progmodes/xref.el
@@ -247,11 +247,9 @@ generic functions.")
247 247
248;;;###autoload 248;;;###autoload
249(defun xref-find-backend () 249(defun xref-find-backend ()
250 (or 250 (run-hook-with-args-until-success 'xref-backend-functions))
251 (run-hook-with-args-until-success 'xref-backend-functions)
252 (user-error "No Xref backend available")))
253 251
254(cl-defgeneric xref-backend-definitions (backend identifier) 252(cl-defgeneric xref-backend-definitions (_backend _identifier)
255 "Find definitions of IDENTIFIER. 253 "Find definitions of IDENTIFIER.
256 254
257The result must be a list of xref objects. If IDENTIFIER 255The result must be a list of xref objects. If IDENTIFIER
@@ -264,7 +262,8 @@ IDENTIFIER can be any string returned by
264`xref-backend-identifier-at-point', or from the table returned by 262`xref-backend-identifier-at-point', or from the table returned by
265`xref-backend-identifier-completion-table'. 263`xref-backend-identifier-completion-table'.
266 264
267To create an xref object, call `xref-make'.") 265To create an xref object, call `xref-make'."
266 (xref--no-backend-available))
268 267
269(cl-defgeneric xref-backend-references (_backend identifier) 268(cl-defgeneric xref-backend-references (_backend identifier)
270 "Find references of IDENTIFIER. 269 "Find references of IDENTIFIER.
@@ -285,12 +284,13 @@ The default implementation uses `xref-references-in-directory'."
285 (xref--project-root pr) 284 (xref--project-root pr)
286 (project-external-roots pr)))))) 285 (project-external-roots pr))))))
287 286
288(cl-defgeneric xref-backend-apropos (backend pattern) 287(cl-defgeneric xref-backend-apropos (_backend _pattern)
289 "Find all symbols that match PATTERN string. 288 "Find all symbols that match PATTERN string.
290The second argument has the same meaning as in `apropos'. 289The second argument has the same meaning as in `apropos'.
291 290
292If BACKEND is implemented in Lisp, it can use 291If BACKEND is implemented in Lisp, it can use
293`xref-apropos-regexp' to convert the pattern to regexp.") 292`xref-apropos-regexp' to convert the pattern to regexp."
293 (xref--no-backend-available))
294 294
295(cl-defgeneric xref-backend-identifier-at-point (_backend) 295(cl-defgeneric xref-backend-identifier-at-point (_backend)
296 "Return the relevant identifier at point. 296 "Return the relevant identifier at point.
@@ -306,8 +306,9 @@ recognize and then delegate the work to an external process."
306 (let ((thing (thing-at-point 'symbol))) 306 (let ((thing (thing-at-point 'symbol)))
307 (and thing (substring-no-properties thing)))) 307 (and thing (substring-no-properties thing))))
308 308
309(cl-defgeneric xref-backend-identifier-completion-table (backend) 309(cl-defgeneric xref-backend-identifier-completion-table (_backend)
310 "Return the completion table for identifiers.") 310 "Return the completion table for identifiers."
311 nil)
311 312
312(cl-defgeneric xref-backend-identifier-completion-ignore-case (_backend) 313(cl-defgeneric xref-backend-identifier-completion-ignore-case (_backend)
313 "Return t if case is not significant in identifier completion." 314 "Return t if case is not significant in identifier completion."
@@ -329,6 +330,10 @@ KEY extracts the key from an element."
329 (cl-loop for key being hash-keys of table using (hash-values value) 330 (cl-loop for key being hash-keys of table using (hash-values value)
330 collect (cons key (nreverse value))))) 331 collect (cons key (nreverse value)))))
331 332
333(defun xref--no-backend-available ()
334 (user-error
335 "No Xref backend. Try `M-x eglot', `M-x visit-tags-table', or `M-x etags-regen-mode'."))
336
332(defun xref--insert-propertized (props &rest strings) 337(defun xref--insert-propertized (props &rest strings)
333 "Insert STRINGS with text properties PROPS." 338 "Insert STRINGS with text properties PROPS."
334 (let ((start (point))) 339 (let ((start (point)))
diff --git a/lisp/replace.el b/lisp/replace.el
index d8b27544128..933249d824c 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -1878,9 +1878,6 @@ is not modified."
1878 (bound-and-true-p ido-everywhere)) 1878 (bound-and-true-p ido-everywhere))
1879 (substitute-command-keys 1879 (substitute-command-keys
1880 "(\\<ido-completion-map>\\[ido-select-text] to end): ")) 1880 "(\\<ido-completion-map>\\[ido-select-text] to end): "))
1881 ((bound-and-true-p icomplete-mode)
1882 (substitute-command-keys
1883 "(\\<icomplete-minibuffer-map>\\[icomplete-exit] to end): "))
1884 ((bound-and-true-p fido-mode) 1881 ((bound-and-true-p fido-mode)
1885 (substitute-command-keys 1882 (substitute-command-keys
1886 "(\\<icomplete-fido-mode-map>\\[icomplete-fido-exit] to end): ")) 1883 "(\\<icomplete-fido-mode-map>\\[icomplete-fido-exit] to end): "))
diff --git a/lisp/subr.el b/lisp/subr.el
index 40325c30326..6f2dcb8c16d 100644
--- a/lisp/subr.el
+++ b/lisp/subr.el
@@ -1148,8 +1148,8 @@ side-effects, and the argument LIST is not modified."
1148 (make-symbol "f"))) 1148 (make-symbol "f")))
1149 (r (make-symbol "r"))) 1149 (r (make-symbol "r")))
1150 `(let (,@(and f `((,f ,pred))) 1150 `(let (,@(and f `((,f ,pred)))
1151 (,tail ,list) 1151 (,r nil)
1152 (,r nil)) 1152 (,tail ,list))
1153 (while (and ,tail (funcall ,(or f pred) (car ,tail))) 1153 (while (and ,tail (funcall ,(or f pred) (car ,tail)))
1154 (push (car ,tail) ,r) 1154 (push (car ,tail) ,r)
1155 (setq ,tail (cdr ,tail))) 1155 (setq ,tail (cdr ,tail)))
@@ -5445,9 +5445,11 @@ If BODY finishes, `while-no-input' returns whatever value BODY produced."
5445 (t val))))))) 5445 (t val)))))))
5446 5446
5447(defmacro condition-case-unless-debug (var bodyform &rest handlers) 5447(defmacro condition-case-unless-debug (var bodyform &rest handlers)
5448 "Like `condition-case' except that it does not prevent debugging. 5448 "Like `condition-case', except that it does not prevent debugging.
5449More specifically if `debug-on-error' is set then the debugger will be invoked 5449More specifically, if `debug-on-error' is set, then the debugger will
5450even if this catches the signal." 5450be invoked even if some handler catches the signal.
5451Note that this doesn't prevent the handler from executing, it just
5452causes the debugger to be called before running the handler."
5451 (declare (debug condition-case) (indent 2)) 5453 (declare (debug condition-case) (indent 2))
5452 `(condition-case ,var 5454 `(condition-case ,var
5453 ,bodyform 5455 ,bodyform
diff --git a/lisp/system-sleep.el b/lisp/system-sleep.el
new file mode 100644
index 00000000000..e09f2fedcd1
--- /dev/null
+++ b/lisp/system-sleep.el
@@ -0,0 +1,513 @@
1;;; system-sleep.el --- System sleep/wake event management -*- lexical-binding: t -*-
2
3;; Copyright (C) 2025-2026 Free Software Foundation, Inc.
4
5;; Author: Stephane Marks <shipmints@gmail.com>
6;; Maintainer: emacs-devel@gnu.org
7;; Keywords: convenience
8;; Package-Requires: ((emacs "31.1"))
9
10;; This file is part of GNU Emacs.
11
12;; GNU Emacs is free software: you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation, either version 3 of the License, or
15;; (at your option) any later version.
16
17;; GNU Emacs is distributed in the hope that it will be useful,
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
21
22;; You should have received a copy of the GNU General Public License
23;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
24
25;;; Commentary:
26
27;; Call `system-sleep-block-sleep' to inhibit system-wide idle sleep.
28;; Idle sleep is typically triggered when the system does not detect
29;; user activity and is independent of any processing that may be on
30;; going. This function is useful to block idle sleep for long-running
31;; operations, for example, when a compilation is running. You have the
32;; option of keeping the system active while letting the display sleep.
33;; This function returns a token which you must use to unblock this
34;; request.
35;;
36;; Call `system-sleep-unblock-sleep' with the token from
37;; `system-sleep-block-sleep' to unblock system-wide idle sleep for this
38;; request. There may be other active requests which will prevent the
39;; system from sleeping.
40;;
41;; The function `system-sleep-sleep-blocked-p' will tell you if
42;; `system-sleep' has any active system sleep blocks.
43;;
44;; Note: When the Emacs process dies, blocks are released on all
45;; platforms.
46;;
47;; You can register functions on the abnormal hook
48;; `system-sleep-event-functions'. Each function will be called when
49;; the system is preparing for sleep and when the system wakes from
50;; sleep. These functions are useful when you want to close (and
51;; potentially reopen) external connections or serial ports.
52;;
53;; On supported GNU/Linux systems, the implementation is via D-Bus to
54;; inhibit idle sleep, keep the display active, and forward events from
55;; logind for system sleep events.
56;;
57;; On macOS and MS-Windows, native APIs are used to block idle sleep,
58;; keep the display active, and provide sleep event notifications.
59;;
60;; On MS-Windows, an idle sleep block that keeps the display active may
61;; not inhibit the screen saver.
62;;
63;; Externally to Emacs, there are system utility functions that you can
64;; use to inspect all processes on your system that might be blocking it
65;; from sleeping.
66;;
67;; On D-Bus systems, you can use the commands:
68;;
69;; systemd-inhibit --list
70;; or
71;; dbus-send --system --print-reply --dest=org.freedesktop.login1 \
72;; /org/freedesktop/login1 \
73;; org.freedesktop.login1.Manager.ListInhibitors
74;;
75;; Note: You can find the sleep/shutdown delay InhibitDelayMaxUSec in
76;; the file logind.conf(5) which typically defaults to 5 seconds.
77;;
78;; On macOS, you can use the command:
79;;
80;; pmset -g assertions
81;;
82;; On MS-Windows, you can use the following command which may need to be
83;; run as an administrator:
84;;
85;; powercfg -requests
86
87;;; Code:
88
89(require 'cl-lib)
90
91;; Pacify the byte compiler.
92(declare-function dbus--fd-close "dbusbind.c")
93(declare-function dbus-unregister-object "dbus.el")
94(declare-function dbus-register-signal "dbus.el")
95(declare-function dbus-call-method "dbus.el")
96(declare-function dbus-list-activatable-names "dbus.el")
97(defvar dbus-service-emacs)
98
99(defgroup system-sleep nil
100 "System sleep/wake blocking and event management."
101 :group 'system-interface
102 :version "31.1")
103
104(defvar system-sleep--back-end nil
105 "Generic sleep-wake method system dispatcher.")
106
107(defvar system-sleep--sleep-block-tokens nil
108 "A list of active sleep-block tokens.
109If non-nil, idle sleep is inhibited by `system-sleep'.")
110
111(cl-defstruct
112 (sleep-event (:type list) :named
113 (:constructor nil)
114 (:constructor make-sleep-event (state)))
115 state)
116
117;;;###autoload
118(defcustom system-sleep-event-functions nil
119 "Abnormal hook invoked on system sleep events.
120Each function is called with one argument EVENT, a sleep event. EVENT
121state can be retrieved via \\+`(sleep-event-state EVENT)'. It will be
122one of the symbols \\+`pre-sleep' or \\+`post-wake'.
123
124Handling \\+`pre-sleep' events should be done as fast as possible, do as
125little as possible and avoid user prompts. Systems often grant a very
126short pre-sleep processing interval, typically ranging between 2 and 5
127seconds. The system may sleep even if your processing is not complete.
128For example, your function could close active connections or serial
129ports.
130
131Handling \\+`post-wake' events offers more leeway. Your function could
132reestablish connections.
133
134Note: Your code, or the functions it calls, should not raise any signals
135or all hooks will be halted preventing other hook functions from
136cleaning up or waking up. You can wrap your code in a `condition-case'
137block."
138 :type 'hook
139 :version "31.1")
140
141;;;###autoload
142(defun system-sleep-block-sleep (&optional why allow-display-sleep)
143 "Inhibit system idle sleep.
144Optional WHY is a string that identifies a sleep block to system utility
145commands that inspect system-wide blocks. WHY defaults to \"Emacs\".
146
147Optional ALLOW-DISPLAY-SLEEP, when non-nil, allows the display to sleep
148or a screen saver to run while the system idle sleep is blocked. The
149default is to keep the display active.
150
151Return a sleep blocking token. You must retain this value and provide
152it to `system-sleep-unblock-sleep' to unblock its associated block.
153
154Return nil if system sleep cannot be inhibited.
155
156Note: All active blocks are released when the Emacs process dies.
157Despite this, you should unblock your blocks when your processing is
158complete. See `with-system-sleep-block' for an easy way to do that."
159 (when system-sleep--back-end
160 (system-sleep--block-sleep (or why "Emacs") allow-display-sleep)))
161
162(defun system-sleep-unblock-sleep (token)
163 "Unblock the system sleep block associated with TOKEN.
164Return non-nil TOKEN was unblocked, or nil if not.
165In the unlikely event that unblock fails, the block will be released
166when the Emacs process dies."
167 (when system-sleep--back-end
168 (system-sleep--unblock-sleep token)))
169
170;;;###autoload
171(defmacro with-system-sleep-block (&optional why allow-display-sleep &rest body)
172 "Execute the forms in BODY while blocking system sleep.
173The optional arguments WHY and ALLOW-DISPLAY-SLEEP have the same meaning
174as in `system-sleep-block-sleep', which see.
175The block is unblocked when BODY completes."
176 (declare (indent 1) (debug t))
177 (let ((token (make-symbol "--sleep-token--")))
178 `(let ((,token (system-sleep-block-sleep ,why ,allow-display-sleep)))
179 (unwind-protect
180 (progn
181 ,@body)
182 (system-sleep-unblock-sleep ,token)))))
183
184(defun system-sleep-unblock-all-sleep-blocks ()
185 "Unblock all `system-sleep' blocks."
186 (while system-sleep--sleep-block-tokens
187 (system-sleep-unblock-sleep (car system-sleep--sleep-block-tokens))))
188
189;;;###autoload
190(defun system-sleep-sleep-blocked-p ()
191 "Return non-nil if there are active sleep blocks."
192 (and system-sleep--back-end
193 system-sleep--sleep-block-tokens))
194
195
196;; Internal implementation.
197
198(defun system-sleep--set-back-end ()
199 "Determine sleep/wake host system type."
200 ;; Order matters to accommodate the cases where an NS or MS-Windows
201 ;; build have the dbus feature.
202 (setq system-sleep--back-end
203 (cond ((featurep 'ns) 'ns)
204 ((featurep 'w32) 'w32)
205 ((and (require 'dbus)
206 (featurep 'dbusbind)
207 (member "org.freedesktop.login1"
208 (dbus-list-activatable-names :system)))
209 'dbus)
210 (t nil))))
211
212(defun system-sleep--sleep-event-handler (event)
213 "`sleep-event' EVENT handler."
214 (declare (completion ignore))
215 (interactive "e")
216 (run-hook-with-args 'system-sleep-event-functions event))
217
218(defun system-sleep-enable ()
219 "Enable `system-sleep'."
220 (unless system-sleep--back-end
221 (if (and (system-sleep--set-back-end)
222 (system-sleep--enable))
223 (keymap-set special-event-map "<sleep-event>"
224 #'system-sleep--sleep-event-handler)
225 (warn "`system-sleep' could not be initialized"))))
226
227(defun system-sleep-disable ()
228 "Disable `system-sleep'."
229 (when system-sleep--back-end
230 (keymap-set special-event-map "<sleep-event>" #'ignore)
231 (system-sleep-unblock-all-sleep-blocks)
232 (system-sleep--disable)
233 (setq system-sleep--back-end nil)))
234
235(cl-defgeneric system-sleep--enable ()
236 "Enable the `system-sleep' back end.
237Return t if the back end is initialized, or nil.")
238
239(cl-defgeneric system-sleep--disable ()
240 "Disable the sleep/wake back end.")
241
242(cl-defgeneric system-sleep--block-sleep (why allow-display-sleep)
243 "Inhibit system idle sleep.
244WHY is a string that identifies a sleep block to system utility commands
245that inspect system-wide blocks.
246When non-nil, ALLOW-DISPLAY-SLEEP allows the display to sleep or a
247screen saver to run while the system idle sleep is blocked. The default
248is to keep the display active.
249Return a sleep-block token.")
250
251(cl-defgeneric system-sleep--unblock-sleep (token)
252 "Unblock the system sleep block associated with TOKEN.
253Return non-nil TOKEN was unblocked, or nil if not.")
254
255(defvar system-sleep--event-in-progress nil)
256(defvar system-sleep--event-queue nil)
257
258(defun system-sleep--sleep-event-function (event)
259 "Handle <sleep-event> special events and avoid races."
260 ;; Queue incoming event.
261 (setq system-sleep--event-queue
262 (append system-sleep--event-queue (list event)))
263 ;; If an event is already in progress, return right away.
264 ;; Otherwise, process queued events.
265 (while (and (not system-sleep--event-in-progress)
266 system-sleep--event-queue)
267 (let ((current-event (pop system-sleep--event-queue)))
268 (setq system-sleep--event-in-progress current-event)
269 (unwind-protect
270 (run-hook-with-args 'system-sleep-event-functions
271 current-event)
272 (setq system-sleep--event-in-progress nil)))))
273
274
275;; D-Bus support.
276
277(defvar system-sleep--dbus-sleep-inhibitor-types "sleep"
278 "This is a colon-separated list of options.
279The default is \"sleep\" which is compatible with the other supported
280`system-sleep' platforms. This could also be
281\"sleep:shutdown\". Shutdown is available only on D-Bus systems.")
282
283(defvar system-sleep--dbus-delay-lock nil)
284(defvar system-sleep--dbus-pre-sleep-signal nil)
285
286(defun system-sleep--dbus-delay-lock (make-or-close)
287 (cond (make-or-close
288 (if system-sleep--dbus-delay-lock
289 (error "Delay lock should be nil")
290 (setq system-sleep--dbus-delay-lock
291 (dbus-call-method
292 :system
293 "org.freedesktop.login1"
294 "/org/freedesktop/login1"
295 "org.freedesktop.login1.Manager"
296 "Inhibit"
297 :keep-fd
298 system-sleep--dbus-sleep-inhibitor-types
299 dbus-service-emacs
300 "Emacs sleep event watcher"
301 "delay"))))
302 (t
303 (when system-sleep--dbus-delay-lock
304 (dbus--fd-close system-sleep--dbus-delay-lock)
305 (setq system-sleep--dbus-delay-lock nil)))))
306
307(defun system-sleep--dbus-prepare-for-sleep-callback (sleep-or-wake)
308 (cond (sleep-or-wake
309 (insert-special-event (make-sleep-event 'pre-sleep)))
310 (t
311 (insert-special-event (make-sleep-event 'post-wake)))))
312
313(defun system-sleep--dbus-prepare-for-sleep-watcher (make-or-close)
314 (cond (make-or-close
315 (if system-sleep--dbus-pre-sleep-signal
316 (error "PrepareForSleep watcher should be nil")
317 (setq system-sleep--dbus-pre-sleep-signal
318 (dbus-register-signal
319 :system
320 "org.freedesktop.login1"
321 "/org/freedesktop/login1"
322 "org.freedesktop.login1.Manager"
323 "PrepareForSleep"
324 #'system-sleep--dbus-prepare-for-sleep-callback))))
325 (t
326 (dbus-unregister-object system-sleep--dbus-pre-sleep-signal)
327 (setq system-sleep--dbus-pre-sleep-signal nil))))
328
329(defun system-sleep--dbus-prepare-for-sleep-function (event)
330 (pcase (sleep-event-state event)
331 ('pre-sleep
332 (system-sleep--dbus-delay-lock nil))
333 ('post-wake
334 (system-sleep--dbus-delay-lock t))))
335
336(cl-defmethod system-sleep--enable (&context
337 (system-sleep--back-end (eql 'dbus)))
338 ;; Order matters.
339 (add-hook 'system-sleep-event-functions
340 #'system-sleep--dbus-prepare-for-sleep-function
341 ;; This must run last.
342 99)
343 (system-sleep--dbus-delay-lock t)
344 (system-sleep--dbus-prepare-for-sleep-watcher t)
345 t)
346
347(cl-defmethod system-sleep--disable (&context
348 (system-sleep--back-end (eql 'dbus)))
349 (system-sleep--dbus-prepare-for-sleep-watcher nil)
350 (system-sleep--dbus-delay-lock nil)
351 (remove-hook 'system-sleep-event-functions
352 #'system-sleep--dbus-prepare-for-sleep-function))
353
354(cl-defmethod system-sleep--block-sleep (why
355 allow-display-sleep
356 &context
357 (system-sleep--back-end (eql 'dbus)))
358 (let ((subtokens))
359 (if-let* ((sleep-cookie (dbus-call-method
360 :system
361 "org.freedesktop.login1"
362 "/org/freedesktop/login1"
363 "org.freedesktop.login1.Manager"
364 "Inhibit"
365 :keep-fd
366 system-sleep--dbus-sleep-inhibitor-types
367 dbus-service-emacs
368 why
369 "block")))
370 (progn
371 (let ((inhibit-quit t))
372 (push (cons 'dbus-inhibitor-lock sleep-cookie) subtokens))
373 (unless allow-display-sleep
374 (if-let* ((screen-cookie
375 (dbus-call-method
376 :session
377 "org.freedesktop.ScreenSaver"
378 "/org/freedesktop/ScreenSaver"
379 "org.freedesktop.ScreenSaver"
380 "Inhibit"
381 dbus-service-emacs
382 "Screen Saver Block")))
383 (let ((inhibit-quit t))
384 (push (cons 'dbus-screensaver-lock screen-cookie) subtokens))
385 (warn "Unable to block the screen saver")))
386 (let ((inhibit-quit t))
387 (let ((token (list :system 'dbus :why why :subtokens subtokens)))
388 (push token system-sleep--sleep-block-tokens)
389 token)))
390 (warn "Unable to block system sleep"))))
391
392(cl-defmethod system-sleep--unblock-sleep (token
393 &context
394 (system-sleep--back-end (eql 'dbus)))
395
396 (if (memq token system-sleep--sleep-block-tokens)
397 (progn
398 (let ((inhibit-quit t))
399 (setq system-sleep--sleep-block-tokens
400 (remq token system-sleep--sleep-block-tokens)))
401 (dolist (subtoken (plist-get token :subtokens))
402 (pcase (car subtoken)
403 ('dbus-inhibitor-lock
404 (dbus--fd-close (cdr subtoken)))
405 ('dbus-screensaver-lock
406 (dbus-call-method
407 :session
408 "org.freedesktop.ScreenSaver"
409 "/org/freedesktop/ScreenSaver"
410 "org.freedesktop.ScreenSaver"
411 "UnInhibit"
412 (cdr subtoken)))))
413 t)
414 (warn "Unknown `system-sleep' sleep token")
415 nil))
416
417
418;; macOS/GNUstep NS support.
419
420(declare-function ns-block-system-sleep "nsfns.m")
421(declare-function ns-unblock-system-sleep "nsfns.m")
422
423(cl-defmethod system-sleep--enable (&context
424 (system-sleep--back-end (eql 'ns)))
425 t)
426
427(cl-defmethod system-sleep--disable (&context
428 (system-sleep--back-end (eql 'ns)))
429 (ignore))
430
431(cl-defmethod system-sleep--block-sleep (why
432 allow-display-sleep
433 &context
434 (system-sleep--back-end (eql 'ns)))
435 (if-let* ((cookie (ns-block-system-sleep why allow-display-sleep))
436 (token (list :system 'ns :why why
437 :token (cons 'ns-sleep-block cookie))))
438 (progn
439 (let ((inhibit-quit t))
440 (push token system-sleep--sleep-block-tokens))
441 token)
442 (warn "Unable to block system sleep")))
443
444(cl-defmethod system-sleep--unblock-sleep (token
445 &context
446 (system-sleep--back-end (eql 'ns)))
447 (if (memq token system-sleep--sleep-block-tokens)
448 (progn
449 (let ((inhibit-quit t))
450 (setq system-sleep--sleep-block-tokens
451 (remq token system-sleep--sleep-block-tokens)))
452 (if (ns-unblock-system-sleep (cdr (plist-get token :token)))
453 t
454 (warn "Unable to unblock system sleep (blocks are released when Emacs dies)")
455 nil))
456 (warn "Unknown `system-sleep' sleep token")
457 nil))
458
459
460;; MS-Windows support.
461
462(declare-function w32-block-system-sleep "w32fns.c")
463(declare-function w32-unblock-system-sleep "w32fns.c")
464(declare-function w32-system-sleep-block-count "w32fns.c")
465
466(defvar system-sleep--w32-sleep-block-count 0)
467
468(cl-defmethod system-sleep--enable (&context
469 (system-sleep--back-end (eql 'w32)))
470 t)
471
472(cl-defmethod system-sleep--disable (&context
473 (system-sleep--back-end (eql 'w32)))
474 (ignore))
475
476(cl-defmethod system-sleep--block-sleep (why
477 allow-display-sleep
478 &context
479 (system-sleep--back-end (eql 'w32)))
480 (if-let* ((cookie (w32-block-system-sleep allow-display-sleep))
481 (token (list :system 'w32 :why why
482 :token (cons 'w32-sleep-block cookie))))
483 (progn
484 (let ((inhibit-quit t))
485 (push token system-sleep--sleep-block-tokens))
486 token)
487 (warn "Unable to block system sleep")))
488
489(cl-defmethod system-sleep--unblock-sleep (token
490 &context
491 (system-sleep--back-end (eql 'w32)))
492 (if (memq token system-sleep--sleep-block-tokens)
493 (progn
494 (let ((inhibit-quit t))
495 (setq system-sleep--sleep-block-tokens
496 (remq token system-sleep--sleep-block-tokens)))
497 (if (eq 0 (w32-system-sleep-block-count))
498 (warn "Unable to unblock system sleep (no active tokens)")
499 (if (w32-unblock-system-sleep)
500 t
501 (warn "Unable to unblock system sleep (blocks are released when Emacs dies)")
502 nil)))
503 (warn "Unknown `system-sleep' sleep token")
504 nil))
505
506
507;; Initialize system-sleep.
508
509(system-sleep-enable)
510
511(provide 'system-sleep)
512
513;;; system-sleep.el ends here
diff --git a/lisp/textmodes/yaml-ts-mode.el b/lisp/textmodes/yaml-ts-mode.el
index 948186b5a9a..c1521c82c22 100644
--- a/lisp/textmodes/yaml-ts-mode.el
+++ b/lisp/textmodes/yaml-ts-mode.el
@@ -48,12 +48,11 @@
48 48
49(defcustom yaml-ts-mode-yamllint-options nil 49(defcustom yaml-ts-mode-yamllint-options nil
50 "Additional options to pass to yamllint command used for Flymake support. 50 "Additional options to pass to yamllint command used for Flymake support.
51If non-nil, this should be a single string with command-line options 51This should be a list of strings, each one passed as a separate argument
52for the yamllint command, with individual options separated by whitespace." 52to the yamllint command."
53 :group 'yaml-ts-mode 53 :group 'yaml-ts-mode
54 :version "31.1" 54 :version "31.1"
55 :type '(choice (const :tag "None" nil) 55 :type '(repeat string))
56 (string :tag "Options as a single string")))
57 56
58(defvar yaml-ts-mode--syntax-table 57(defvar yaml-ts-mode--syntax-table
59 (let ((table (make-syntax-table))) 58 (let ((table (make-syntax-table)))
@@ -199,10 +198,7 @@ Calls REPORT-FN directly."
199 (when (process-live-p yaml-ts-mode--flymake-process) 198 (when (process-live-p yaml-ts-mode--flymake-process)
200 (kill-process yaml-ts-mode--flymake-process)) 199 (kill-process yaml-ts-mode--flymake-process))
201 (let ((yamllint (executable-find "yamllint")) 200 (let ((yamllint (executable-find "yamllint"))
202 (params (if yaml-ts-mode-yamllint-options 201 (params (append yaml-ts-mode-yamllint-options '("-f" "parsable" "-")))
203 (append (split-string yaml-ts-mode-yamllint-options) '("-f" "parsable" "-"))
204 '("-f" "parsable" "-")))
205
206 (source (current-buffer)) 202 (source (current-buffer))
207 (diagnostics-pattern (eval-when-compile 203 (diagnostics-pattern (eval-when-compile
208 (rx bol (+? nonl) ":" ; every diagnostic line start with the filename 204 (rx bol (+? nonl) ":" ; every diagnostic line start with the filename
diff --git a/lisp/time.el b/lisp/time.el
index c78a51e9f97..f553ebab413 100644
--- a/lisp/time.el
+++ b/lisp/time.el
@@ -177,6 +177,18 @@ depend on `display-time-day-and-date' and `display-time-24hr-format'."
177 :type '(choice (const :tag "Default" nil) 177 :type '(choice (const :tag "Default" nil)
178 string)) 178 string))
179 179
180(defcustom display-time-help-echo-format "%a %b %e, %Y"
181 "Format for the help echo when hovering over the time in the mode line.
182Use the function `customize-variable' to choose a common format, and/or
183see the function `format-time-string' for an explanation of the syntax."
184 :version "31.1"
185 :type `(choice
186 ,@(mapcar #'(lambda (fmt)
187 (list 'const
188 ':tag (format-time-string fmt 0 "UTC") fmt))
189 '("%a %b %e, %Y" "%F (%a)" "%a %D"))
190 (string :tag "Format string")))
191
180(defcustom display-time-string-forms 192(defcustom display-time-string-forms
181 '((if (and (not display-time-format) display-time-day-and-date) 193 '((if (and (not display-time-format) display-time-day-and-date)
182 (format-time-string "%a %b %e " now) 194 (format-time-string "%a %b %e " now)
@@ -186,7 +198,9 @@ depend on `display-time-day-and-date' and `display-time-24hr-format'."
186 (if display-time-24hr-format "%H:%M" "%-I:%M%p")) 198 (if display-time-24hr-format "%H:%M" "%-I:%M%p"))
187 now) 199 now)
188 'face 'display-time-date-and-time 200 'face 'display-time-date-and-time
189 'help-echo (format-time-string "%a %b %e, %Y" now)) 201 'help-echo (format-time-string (if (stringp display-time-help-echo-format)
202 display-time-help-echo-format
203 "%a %b %e, %Y") now))
190 load 204 load
191 (if mail 205 (if mail
192 ;; Build the string every time to act on customization. 206 ;; Build the string every time to act on customization.
diff --git a/lisp/tutorial.el b/lisp/tutorial.el
index c071c1ff1d8..6ade473c975 100644
--- a/lisp/tutorial.el
+++ b/lisp/tutorial.el
@@ -69,18 +69,17 @@ Where
69 WHERE is a text describing the key sequences to which DEF-FUN is 69 WHERE is a text describing the key sequences to which DEF-FUN is
70 bound now (or, if it is remapped, a key sequence 70 bound now (or, if it is remapped, a key sequence
71 for the function it is remapped to)" 71 for the function it is remapped to)"
72 (with-output-to-temp-buffer (help-buffer) 72 (help-setup-xref (list #'tutorial--describe-nonstandard-key value)
73 (help-setup-xref (list #'tutorial--describe-nonstandard-key value) 73 (called-interactively-p 'interactive))
74 (called-interactively-p 'interactive)) 74 (with-help-window (help-buffer)
75 (with-current-buffer (help-buffer) 75 (insert
76 (insert 76 "Your Emacs customizations override the default binding for this key:"
77 "Your Emacs customizations override the default binding for this key:" 77 "\n\n")
78 "\n\n") 78 (let ((inhibit-read-only t))
79 (let ((inhibit-read-only t)) 79 (cond
80 (cond 80 ((eq (car value) 'cua-mode)
81 ((eq (car value) 'cua-mode) 81 (insert
82 (insert 82 "CUA mode is enabled.
83 "CUA mode is enabled.
84 83
85When CUA mode is enabled, you can use C-z, C-x, C-c, and C-v to 84When CUA mode is enabled, you can use C-z, C-x, C-c, and C-v to
86undo, cut, copy, and paste in addition to the normal Emacs 85undo, cut, copy, and paste in addition to the normal Emacs
@@ -94,70 +93,70 @@ options:
94- press the prefix key twice very quickly (within 0.2 seconds), 93- press the prefix key twice very quickly (within 0.2 seconds),
95- press the prefix key and the following key within 0.2 seconds, or 94- press the prefix key and the following key within 0.2 seconds, or
96- use the SHIFT key with the prefix key, i.e. C-S-x or C-S-c.")) 95- use the SHIFT key with the prefix key, i.e. C-S-x or C-S-c."))
97 ((eq (car value) 'current-binding) 96 ((eq (car value) 'current-binding)
98 (let ((cb (nth 1 value)) 97 (let ((cb (nth 1 value))
99 (db (nth 2 value)) 98 (db (nth 2 value))
100 (key (nth 3 value)) 99 (key (nth 3 value))
101 (where (nth 4 value)) 100 (where (nth 4 value))
102 map 101 map
103 (maps (current-active-maps)) 102 (maps (current-active-maps))
104 mapsym) 103 mapsym)
105 ;; Look at the currently active keymaps and try to find 104 ;; Look at the currently active keymaps and try to find
106 ;; first the keymap where the current binding occurs: 105 ;; first the keymap where the current binding occurs:
107 (while maps 106 (while maps
108 (let* ((m (car maps)) 107 (let* ((m (car maps))
109 (mb (lookup-key m key t))) 108 (mb (lookup-key m key t)))
110 (setq maps (cdr maps)) 109 (setq maps (cdr maps))
111 (when (eq mb cb) 110 (when (eq mb cb)
112 (setq map m) 111 (setq map m)
113 (setq maps nil)))) 112 (setq maps nil))))
114 ;; Now, if a keymap was found we must found the symbol 113 ;; Now, if a keymap was found we must found the symbol
115 ;; name for it to display to the user. This can not 114 ;; name for it to display to the user. This can not
116 ;; always be found since all keymaps does not have a 115 ;; always be found since all keymaps does not have a
117 ;; symbol pointing to them, but here they should have 116 ;; symbol pointing to them, but here they should have
118 ;; that: 117 ;; that:
119 (when map 118 (when map
120 (mapatoms (lambda (s) 119 (mapatoms (lambda (s)
121 (and 120 (and
122 ;; If not already found 121 ;; If not already found
123 (not mapsym) 122 (not mapsym)
124 ;; and if s is a keymap 123 ;; and if s is a keymap
125 (and (boundp s) 124 (and (boundp s)
126 (keymapp (symbol-value s))) 125 (keymapp (symbol-value s)))
127 ;; and not the local symbol map 126 ;; and not the local symbol map
128 (not (eq s 'map)) 127 (not (eq s 'map))
129 ;; and the value of s is map 128 ;; and the value of s is map
130 (eq map (symbol-value s)) 129 (eq map (symbol-value s))
131 ;; then save this value in mapsym 130 ;; then save this value in mapsym
132 (setq mapsym s))))) 131 (setq mapsym s)))))
133 (insert 132 (insert
134 (format-message 133 (format-message
135 "The default Emacs binding for the key %s is the command `%s'. " 134 "The default Emacs binding for the key %s is the command `%s'. "
136 (key-description key) 135 (key-description key)
137 db)) 136 db))
138 (insert "However, your customizations have " 137 (insert "However, your customizations have "
139 (if cb 138 (if cb
140 (format-message "rebound it to the command `%s'" cb) 139 (format-message "rebound it to the command `%s'" cb)
141 "unbound it")) 140 "unbound it"))
142 (insert ".") 141 (insert ".")
143 (when mapsym 142 (when mapsym
144 (insert " (For the more advanced user:" 143 (insert " (For the more advanced user:"
145 (format-message 144 (format-message
146 " This binding is in the keymap `%s'.)" mapsym))) 145 " This binding is in the keymap `%s'.)" mapsym)))
147 (if (string= where "") 146 (if (string= where "")
148 (unless (keymapp db) 147 (unless (keymapp db)
149 (insert "\n\nYou can use M-x " 148 (insert "\n\nYou can use M-x "
150 (format "%s" db) 149 (format "%s" db)
151 " RET instead.")) 150 " RET instead."))
152 (insert "\n\nWith your current key bindings" 151 (insert "\n\nWith your current key bindings"
153 " you can use " 152 " you can use "
154 (if (string-match-p "^the .*menus?$" where) 153 (if (string-match-p "^the .*menus?$" where)
155 "" 154 ""
156 "the key ") 155 "the key ")
157 where 156 where
158 (format-message " to get the function `%s'." db)))) 157 (format-message " to get the function `%s'." db))))
159 (fill-region (point-min) (point))))) 158 (fill-region (point-min) (point)))))
160 (help-print-return-message)))) 159 (help-print-return-message)))
161 160
162(defconst tutorial--default-keys 161(defconst tutorial--default-keys
163 (eval-when-compile 162 (eval-when-compile
@@ -272,71 +271,70 @@ options:
272 271
273(defun tutorial--detailed-help (button) 272(defun tutorial--detailed-help (button)
274 "Give detailed help about changed keys." 273 "Give detailed help about changed keys."
275 (with-output-to-temp-buffer (help-buffer) 274 (help-setup-xref (list #'tutorial--detailed-help button)
276 (help-setup-xref (list #'tutorial--detailed-help button) 275 (called-interactively-p 'interactive))
277 (called-interactively-p 'interactive)) 276 (with-help-window (help-buffer)
278 (with-current-buffer (help-buffer) 277 (let* ((tutorial-buffer (button-get button 'tutorial-buffer))
279 (let* ((tutorial-buffer (button-get button 'tutorial-buffer)) 278 (explain-key-desc (button-get button 'explain-key-desc))
280 (explain-key-desc (button-get button 'explain-key-desc)) 279 (changed-keys (with-current-buffer tutorial-buffer
281 (changed-keys (with-current-buffer tutorial-buffer 280 (save-excursion
282 (save-excursion 281 (goto-char (point-min))
283 (goto-char (point-min)) 282 (tutorial--find-changed-keys
284 (tutorial--find-changed-keys 283 tutorial--default-keys)))))
285 tutorial--default-keys))))) 284 (when changed-keys
286 (when changed-keys 285 (insert
287 (insert 286 "The following key bindings used in the tutorial have been changed
288 "The following key bindings used in the tutorial have been changed
289from the Emacs default:\n\n" ) 287from the Emacs default:\n\n" )
290 (let ((frm " %-14s %-27s %-16s\n")) 288 (let ((frm " %-14s %-27s %-16s\n"))
291 (insert (format frm 289 (insert (format frm
292 "Standard Key" "Command" "In Your Emacs"))) 290 "Standard Key" "Command" "In Your Emacs")))
293 (dolist (tk changed-keys) 291 (dolist (tk changed-keys)
294 (let* ((def-fun (nth 1 tk)) 292 (let* ((def-fun (nth 1 tk))
295 (key (nth 0 tk)) 293 (key (nth 0 tk))
296 (def-fun-txt (nth 2 tk)) 294 (def-fun-txt (nth 2 tk))
297 (where (nth 3 tk)) 295 (where (nth 3 tk))
298 (remark (nth 4 tk)) 296 (remark (nth 4 tk))
299 (key-txt (key-description key)) 297 (key-txt (key-description key))
300 (key-fun (with-current-buffer tutorial-buffer (key-binding key)))) 298 (key-fun (with-current-buffer tutorial-buffer (key-binding key))))
301 (unless (eq def-fun key-fun) 299 (unless (eq def-fun key-fun)
302 ;; Insert key binding description: 300 ;; Insert key binding description:
303 (when (string= key-txt explain-key-desc) 301 (when (string= key-txt explain-key-desc)
304 (put-text-property 0 (length key-txt) 302 (put-text-property 0 (length key-txt)
305 'face 'tutorial-warning-face key-txt)) 303 'face 'tutorial-warning-face key-txt))
306 (insert " " key-txt " ") 304 (insert " " key-txt " ")
307 (indent-to 18) 305 (indent-to 18)
308 ;; Insert a link describing the old binding: 306 ;; Insert a link describing the old binding:
309 (insert-button def-fun-txt 307 (insert-button def-fun-txt
310 'value def-fun 308 'value def-fun
311 'action 309 'action
312 (lambda (button) (interactive) 310 (lambda (button) (interactive)
313 (describe-function 311 (describe-function
314 (button-get button 'value))) 312 (button-get button 'value)))
315 'follow-link t) 313 'follow-link t)
316 (indent-to 45) 314 (indent-to 45)
317 (when (listp where) 315 (when (listp where)
318 (setq where "list")) 316 (setq where "list"))
319 ;; Tell where the old binding is now: 317 ;; Tell where the old binding is now:
320 (insert (format " %-16s " 318 (insert (format " %-16s "
321 (if (string= "" where) 319 (if (string= "" where)
322 (format "M-x %s" def-fun-txt) 320 (format "M-x %s" def-fun-txt)
323 where))) 321 where)))
324 ;; Insert a link with more information, for example 322 ;; Insert a link with more information, for example
325 ;; current binding and keymap or information about 323 ;; current binding and keymap or information about
326 ;; cua-mode replacements: 324 ;; cua-mode replacements:
327 (insert-button (car remark) 325 (insert-button (car remark)
328 'action 326 'action
329 (lambda (b) (interactive) 327 (lambda (b) (interactive)
330 (let ((value (button-get b 'value))) 328 (let ((value (button-get b 'value)))
331 (tutorial--describe-nonstandard-key value))) 329 (tutorial--describe-nonstandard-key value)))
332 'value (cdr remark) 330 'value (cdr remark)
333 'follow-link t) 331 'follow-link t)
334 (insert "\n"))))) 332 (insert "\n")))))
335 333
336 (insert " 334 (insert "
337It is OK to change key bindings, but changed bindings do not 335It is OK to change key bindings, but changed bindings do not
338correspond to what the tutorial says.\n\n") 336correspond to what the tutorial says.\n\n")
339 (help-print-return-message))))) 337 (help-print-return-message))))
340 338
341(defun tutorial--find-changed-keys (default-keys) 339(defun tutorial--find-changed-keys (default-keys)
342 "Find the key bindings used in the tutorial that have changed. 340 "Find the key bindings used in the tutorial that have changed.
diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index 5c0fb5fba4c..559310ff770 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -201,8 +201,9 @@ The default \"-b\" means to ignore whitespace-only changes,
201(defvar-keymap diff-mode-shared-map 201(defvar-keymap diff-mode-shared-map
202 :doc "Bindings for read-only `diff-mode' buffers. 202 :doc "Bindings for read-only `diff-mode' buffers.
203These bindings are also available with an ESC prefix 203These bindings are also available with an ESC prefix
204(i.e. a \\=`M-' prefix) in read-write `diff-mode' buffers, 204(i.e. a \\=`M-' prefix) in all `diff-mode' buffers, including in
205and with a `diff-minor-mode-prefix' prefix in `diff-minor-mode'. 205particular read-write `diff-mode' buffers, and with a
206`diff-minor-mode-prefix' prefix in `diff-minor-mode'.
206See also `diff-mode-read-only-map'." 207See also `diff-mode-read-only-map'."
207 "n" #'diff-hunk-next 208 "n" #'diff-hunk-next
208 "N" #'diff-file-next 209 "N" #'diff-file-next
@@ -217,14 +218,7 @@ See also `diff-mode-read-only-map'."
217 "RET" #'diff-goto-source 218 "RET" #'diff-goto-source
218 "<mouse-2>" #'diff-goto-source 219 "<mouse-2>" #'diff-goto-source
219 "o" #'diff-goto-source ; other-window 220 "o" #'diff-goto-source ; other-window
220 "<remap> <undo>" #'undo-ignore-read-only 221 "<remap> <undo>" #'undo-ignore-read-only)
221
222 ;; The foregoing commands don't affect buffers beyond this one.
223 ;; The following command is the only one that has a single-letter
224 ;; binding and which affects buffers beyond this one.
225 ;; However, the following command asks for confirmation by default,
226 ;; so that seems okay. --spwhitton
227 "u" #'diff-revert-and-kill-hunk)
228 222
229;; Not `diff-read-only-mode-map' because there is no such mode 223;; Not `diff-read-only-mode-map' because there is no such mode
230;; `diff-read-only-mode'; see comment above. 224;; `diff-read-only-mode'; see comment above.
@@ -233,15 +227,28 @@ See also `diff-mode-read-only-map'."
233 :doc "Additional bindings for read-only `diff-mode' buffers. 227 :doc "Additional bindings for read-only `diff-mode' buffers.
234Most of the bindings for read-only `diff-mode' buffers are in 228Most of the bindings for read-only `diff-mode' buffers are in
235`diff-mode-shared-map'. This map contains additional bindings for 229`diff-mode-shared-map'. This map contains additional bindings for
236read-only `diff-mode' buffers that are *not* available with an ESC 230read-only `diff-mode' buffers that are *not* also available with an ESC
237prefix (i.e. a \\=`M-' prefix) in read-write `diff-mode' buffers." 231prefix (i.e. a \\=`M-' prefix) in read-write (nor read-only) `diff-mode'
232buffers."
238 ;; We don't want the following in read-write `diff-mode' buffers 233 ;; We don't want the following in read-write `diff-mode' buffers
239 ;; because they hide useful `M-<foo>' global bindings when editing. 234 ;; because they hide useful `M-<foo>' global bindings when editing.
240 "W" #'widen 235 "W" #'widen
241 "w" #'diff-kill-ring-save 236 "w" #'diff-kill-ring-save
242 "A" #'diff-ediff-patch 237 "A" #'diff-ediff-patch
243 "r" #'diff-restrict-view 238 "r" #'diff-restrict-view
244 "R" #'diff-reverse-direction) 239 "R" #'diff-reverse-direction
240 "s" #'diff-split-hunk
241
242 ;; The foregoing commands in `diff-mode-shared-map' and
243 ;; `diff-mode-read-only-map' don't affect buffers beyond this one.
244 ;; The following command is the only one that has a single-character
245 ;; binding and which affects buffers beyond this one. However, the
246 ;; following command asks for confirmation by default, so that seems
247 ;; okay. --spwhitton
248 "u" #'diff-revert-and-kill-hunk
249 ;; `diff-revert-and-kill-hunk' is the `diff-mode' analogue of what '@'
250 ;; does in VC-Dir, so give it the same short binding.
251 "@" #'diff-revert-and-kill-hunk)
245 252
246(defvar-keymap diff-mode-map 253(defvar-keymap diff-mode-map
247 :doc "Keymap for `diff-mode'. See also `diff-mode-shared-map'." 254 :doc "Keymap for `diff-mode'. See also `diff-mode-shared-map'."
@@ -882,31 +889,19 @@ If the prefix ARG is given, restrict the view to the current file instead."
882 (goto-char (point-min)) 889 (goto-char (point-min))
883 (re-search-forward diff-hunk-header-re nil t))) 890 (re-search-forward diff-hunk-header-re nil t)))
884 891
885(defun diff-hunk-kill () 892(defun diff-hunk-kill (&optional beg end)
886 "Kill the hunk at point." 893 "Kill the hunk at point.
887 (interactive) 894When killing the last hunk left for a file, kill the file header too.
888 (if (not (diff--some-hunks-p)) 895Interactively, if the region is active, kill all hunks that the region
889 (error "No hunks") 896overlaps.
890 (diff-beginning-of-hunk t) 897
891 (let* ((hunk-bounds (diff-bounds-of-hunk)) 898When called from Lisp with optional arguments BEG and END non-nil, kill
892 (file-bounds (ignore-errors (diff-bounds-of-file))) 899all hunks overlapped by the region from BEG to END as though called
893 ;; If the current hunk is the only one for its file, kill the 900interactively with an active region delimited by BEG and END."
894 ;; file header too. 901 (interactive "R")
895 (bounds (if (and file-bounds 902 (when (xor beg end)
896 (progn (goto-char (car file-bounds)) 903 (error "Invalid call to `diff-hunk-kill'"))
897 (= (progn (diff-hunk-next) (point)) 904 (diff--revert-kill-hunks beg end nil))
898 (car hunk-bounds)))
899 (progn (goto-char (cadr hunk-bounds))
900 ;; bzr puts a newline after the last hunk.
901 (while (looking-at "^\n")
902 (forward-char 1))
903 (= (point) (cadr file-bounds))))
904 file-bounds
905 hunk-bounds))
906 (inhibit-read-only t))
907 (apply #'kill-region bounds)
908 (goto-char (car bounds))
909 (ignore-errors (diff-beginning-of-hunk t)))))
910 905
911;; This is not `diff-kill-other-hunks' because we might need to make 906;; This is not `diff-kill-other-hunks' because we might need to make
912;; copies of file headers in order to ensure the new kill ring entry 907;; copies of file headers in order to ensure the new kill ring entry
@@ -2282,6 +2277,83 @@ With a prefix argument, try to REVERSE the hunk."
2282 :type 'boolean 2277 :type 'boolean
2283 :version "31.1") 2278 :version "31.1")
2284 2279
2280(defun diff--revert-kill-hunks (beg end revertp)
2281 "Workhorse routine for killing hunks, after possibly reverting them.
2282If BEG and END are nil, kill the hunk at point.
2283Otherwise kill all hunks overlapped by region delimited by BEG and END.
2284When killing a hunk that's the only one remaining for its file, kill the
2285file header too.
2286If REVERTP is non-nil, reverse-apply hunks before killing them."
2287 ;; With BEG and END non-nil, we push each hunk to the kill ring
2288 ;; separately. If we want to push to the kill ring just once, we have
2289 ;; to decide how to handle file headers such that the meanings of the
2290 ;; hunks in the kill ring entry, considered as a whole patch, do not
2291 ;; deviate too far from the meanings the hunks had in this buffer.
2292 ;;
2293 ;; For example, if we have a single hunk for one file followed by
2294 ;; multiple hunks for another file, and we naïvely kill the single
2295 ;; hunk and the first of the multiple hunks, our kill ring entry will
2296 ;; be a patch applying those two hunks to the first file. This is
2297 ;; because killing the single hunk will have brought its file header
2298 ;; with it, but not so killing the second hunk. So we will have put
2299 ;; together hunks that were previously for two different files.
2300 ;;
2301 ;; One option is to *copy* every file header that the region overlaps
2302 ;; (and that we will not kill, because we are leaving other hunks for
2303 ;; that file behind). But then the text this command pushes to the
2304 ;; kill ring would be different from the text it removes from the
2305 ;; buffer, which would be unintuitive for an Emacs kill command.
2306 ;;
2307 ;; An alternative might be to have restrictions as follows:
2308 ;;
2309 ;; Interactively, if the region is active, try to kill all hunks that the
2310 ;; region overlaps. This works when either
2311 ;; - all the hunks the region overlaps are for the same file; or
2312 ;; - the last hunk the region overlaps is the last hunk for its file.
2313 ;; These restrictions are so that the text added to the kill ring does not
2314 ;; merge together hunks for different files under a single file header.
2315 ;;
2316 ;; We would error out if neither property is met. When either holds,
2317 ;; any file headers the region overlaps are ones we should kill.
2318 (unless (diff--some-hunks-p)
2319 (error "No hunks"))
2320 (if beg
2321 (save-excursion
2322 (goto-char beg)
2323 (setq beg (car (diff-bounds-of-hunk)))
2324 (goto-char end)
2325 (unless (looking-at diff-hunk-header-re)
2326 (setq end (cadr (diff-bounds-of-hunk)))))
2327 (pcase-setq `(,beg ,end) (diff-bounds-of-hunk)))
2328 (when (or (not revertp) (null (diff-apply-buffer beg end t)))
2329 (goto-char end)
2330 (when-let* ((pos (diff--at-diff-header-p)))
2331 (goto-char pos))
2332 (setq beg (copy-marker beg) end (point-marker))
2333 (unwind-protect
2334 (cl-loop initially (goto-char beg)
2335 with inhibit-read-only = t
2336 for (hunk-beg hunk-end) = (diff-bounds-of-hunk)
2337 for file-bounds = (ignore-errors (diff-bounds-of-file))
2338 for (file-beg file-end) = file-bounds
2339 if (and file-bounds
2340 (progn
2341 (goto-char file-beg)
2342 (diff-hunk-next)
2343 (eq (point) hunk-beg))
2344 (progn
2345 (goto-char hunk-end)
2346 ;; bzr puts a newline after the last hunk.
2347 (while (looking-at "^\n") (forward-char 1))
2348 (eq (point) file-end)))
2349 do (kill-region file-beg file-end) (goto-char file-beg)
2350 else do (kill-region hunk-beg hunk-end) (goto-char hunk-beg)
2351 do (ignore-errors (diff-beginning-of-hunk t))
2352 until (or (< (point) (marker-position beg))
2353 (eql (point) (marker-position end))))
2354 (set-marker beg nil)
2355 (set-marker end nil))))
2356
2285(defun diff-revert-and-kill-hunk (&optional beg end) 2357(defun diff-revert-and-kill-hunk (&optional beg end)
2286 "Reverse-apply and then kill the hunk at point. Save changed buffer. 2358 "Reverse-apply and then kill the hunk at point. Save changed buffer.
2287Interactively, if the region is active, reverse-apply and kill all 2359Interactively, if the region is active, reverse-apply and kill all
@@ -2307,27 +2379,7 @@ BEG and END."
2307 (error "Invalid call to `diff-revert-and-kill-hunk'")) 2379 (error "Invalid call to `diff-revert-and-kill-hunk'"))
2308 (when (or (not diff-ask-before-revert-and-kill-hunk) 2380 (when (or (not diff-ask-before-revert-and-kill-hunk)
2309 (y-or-n-p "Really reverse-apply and kill hunk(s)?")) 2381 (y-or-n-p "Really reverse-apply and kill hunk(s)?"))
2310 (if beg 2382 (diff--revert-kill-hunks beg end t)))
2311 (save-excursion
2312 (goto-char beg)
2313 (setq beg (car (diff-bounds-of-hunk)))
2314 (goto-char end)
2315 (unless (looking-at diff-hunk-header-re)
2316 (setq end (cadr (diff-bounds-of-hunk)))))
2317 (pcase-setq `(,beg ,end) (diff-bounds-of-hunk)))
2318 (when (null (diff-apply-buffer beg end t))
2319 ;; Use `diff-hunk-kill' because it properly handles file headers.
2320 (goto-char end)
2321 (when-let* ((pos (diff--at-diff-header-p)))
2322 (goto-char pos))
2323 (setq beg (copy-marker beg) end (point-marker))
2324 (unwind-protect
2325 (cl-loop initially (goto-char beg)
2326 do (diff-hunk-kill)
2327 until (or (< (point) (marker-position beg))
2328 (eql (point) (marker-position end))))
2329 (set-marker beg nil)
2330 (set-marker end nil)))))
2331 2383
2332(defun diff-apply-buffer (&optional beg end reverse test-or-no-save) 2384(defun diff-apply-buffer (&optional beg end reverse test-or-no-save)
2333 "Apply the diff in the entire diff buffer. 2385 "Apply the diff in the entire diff buffer.
diff --git a/lisp/vc/vc-dispatcher.el b/lisp/vc/vc-dispatcher.el
index dc17b582ed7..2015e7540ae 100644
--- a/lisp/vc/vc-dispatcher.el
+++ b/lisp/vc/vc-dispatcher.el
@@ -384,6 +384,9 @@ the man pages for \"torsocks\" for more details about Tor."
384 :version "27.1" 384 :version "27.1"
385 :group 'vc) 385 :group 'vc)
386 386
387(defvar vc-user-edit-command-history nil
388 "Name of minibuffer history variable for `vc-user-edit-command'.")
389
387(defun vc-user-edit-command (command file-or-list flags) 390(defun vc-user-edit-command (command file-or-list flags)
388 "Prompt the user to edit VC command COMMAND and FLAGS. 391 "Prompt the user to edit VC command COMMAND and FLAGS.
389Intended to be used as the value of `vc-filter-command-function'." 392Intended to be used as the value of `vc-filter-command-function'."
@@ -398,7 +401,8 @@ Intended to be used as the value of `vc-filter-command-function'."
398 (cons command (remq nil (if files-separator-p 401 (cons command (remq nil (if files-separator-p
399 (butlast flags) 402 (butlast flags)
400 flags)))) 403 flags))))
401 " "))))) 404 " ")
405 vc-user-edit-command-history))))
402 (list (car edited) file-or-list 406 (list (car edited) file-or-list
403 (nconc (cdr edited) (and files-separator-p '("--")))))) 407 (nconc (cdr edited) (and files-separator-p '("--"))))))
404 408
diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el
index 73db9c0f181..5e51b28fb37 100644
--- a/lisp/vc/vc-git.el
+++ b/lisp/vc/vc-git.el
@@ -772,70 +772,91 @@ or an empty string if none."
772 (vc-git--out-match '("symbolic-ref" "HEAD") 772 (vc-git--out-match '("symbolic-ref" "HEAD")
773 "^\\(refs/heads/\\)?\\(.+\\)$" 2)) 773 "^\\(refs/heads/\\)?\\(.+\\)$" 2))
774 774
775(defun vc-git--branch-remotes ()
776 "Return alist of configured remote branches for current branch.
777If there is a configured upstream, return the remote-tracking branch
778with key `upstream'. If there is a distinct configured push remote,
779return the remote-tracking branch there with key `push'.
780A configured push remote that's just the same as the upstream remote is
781ignored because that means we're not actually in a triangular workflow."
782 ;; Possibly we could simplify this using @{push}, but that may involve
783 ;; an unwanted dependency on the setting of push.default.
784 (cl-flet ((get (key)
785 (string-trim-right (vc-git--out-str "config" key))))
786 (let* ((branch (vc-git-working-branch))
787 (pull (get (format "branch.%s.remote" branch)))
788 (merge (string-remove-prefix "refs/heads/"
789 (get (format "branch.%s.merge"
790 branch))))
791 (push (get (format "branch.%s.pushRemote" branch)))
792 (push (if (string-empty-p push)
793 (get "remote.pushDefault")
794 push))
795 (alist (and (not (string-empty-p pull))
796 (not (string-empty-p merge))
797 `((upstream . ,(format "%s/%s" pull merge))))))
798 (if (or (string-empty-p push) (equal push pull))
799 alist
800 (cl-acons 'push (format "%s/%s" push branch) alist)))))
801
775(defun vc-git-trunk-or-topic-p () 802(defun vc-git-trunk-or-topic-p ()
776 "Return `topic' if branch has distinct pull and push remotes, else nil. 803 "Return `topic' if branch has distinct pull and push remotes, else nil.
777This is able to identify topic branches for certain forge workflows." 804This is able to identify topic branches for certain forge workflows."
778 (let* ((branch (vc-git-working-branch)) 805 (let ((remotes (vc-git--branch-remotes)))
779 (merge (string-trim-right 806 (and (assq 'upstream remotes) (assq 'push remotes) 'topic)))
780 (vc-git--out-str "config" (format "branch.%s.remote"
781 branch))))
782 (push (string-trim-right
783 (vc-git--out-str "config" (format "branch.%s.pushRemote"
784 branch))))
785 (push (if (string-empty-p push)
786 (string-trim-right
787 (vc-git--out-str "config" "remote.pushDefault"))
788 push)))
789 (and (plusp (length merge))
790 (plusp (length push))
791 (not (equal merge push))
792 'topic)))
793 807
794(defun vc-git-topic-outgoing-base () 808(defun vc-git-topic-outgoing-base ()
795 "Return the outgoing base for the current branch as a string. 809 "Return the outgoing base for the current branch as a string.
796This works by considering the current branch as a topic branch 810This works by considering the current branch as a topic branch
797(whether or not it actually is). 811(whether or not it actually is).
798Requires that the corresponding trunk exists as a local branch. 812
799 813If there is a distinct push remote for this branch, assume the target
800The algorithm employed is as follows. Find all merge bases between the 814for outstanding changes is the tracking branch, and return that.
801current branch and other local branches. Each of these is a commit on 815
802the current branch. Use `git merge-base --independent' on them all to 816Otherwise, fall back to the following algorithm, which requires that the
803find the topologically most recent. Take the branch for which that 817corresponding trunk exists as a local branch. Find all merge bases
804commit is a merge base with the current branch to be the branch into 818between the current branch and other local branches. Each of these is a
805which the current branch will eventually be merged. Find its upstream. 819commit on the current branch. Use `git merge-base --independent' on
806(If there is more than one branch whose merge base with the current 820them all to find the topologically most recent. Take the branch for
807branch is that same topologically most recent commit, try them 821which that commit is a merge base with the current branch to be the
808one-by-one, accepting the first that has an upstream.)" 822branch into which the current branch will eventually be merged. Find
809 (cl-flet ((get-line () (buffer-substring (point) (pos-eol)))) 823its upstream. (If there is more than one branch whose merge base with
810 (let* ((branches (vc-git-branches)) 824the current branch is that same topologically most recent commit, try
811 (current (pop branches)) 825them one-by-one, accepting the first that has an upstream.)"
812 merge-bases) 826 (if-let* ((remotes (vc-git--branch-remotes))
813 (with-temp-buffer 827 (_ (assq 'push remotes))
814 (dolist (branch branches) 828 (upstream (assq 'upstream remotes)))
815 (erase-buffer) 829 (cdr upstream)
816 (when (vc-git--out-ok "merge-base" "--all" branch current) 830 (cl-flet ((get-line () (buffer-substring (point) (pos-eol))))
817 (goto-char (point-min)) 831 (let* ((branches (vc-git-branches))
818 (while (not (eobp)) 832 (current (pop branches))
819 (push branch 833 merge-bases)
820 (alist-get (get-line) merge-bases nil nil #'equal)) 834 (with-temp-buffer
821 (forward-line 1)))) 835 (dolist (branch branches)
822 (erase-buffer)
823 (unless (apply #'vc-git--out-ok "merge-base" "--independent"
824 (mapcar #'car merge-bases))
825 (error "`git merge-base --independent' failed"))
826 ;; If 'git merge-base --independent' printed more than one line,
827 ;; just pick the first.
828 (goto-char (point-min))
829 (catch 'ret
830 (dolist (target (cdr (assoc (get-line) merge-bases)))
831 (erase-buffer) 836 (erase-buffer)
832 (when (vc-git--out-ok "for-each-ref" 837 (when (vc-git--out-ok "merge-base" "--all" branch current)
833 "--format=%(upstream:short)"
834 (concat "refs/heads/" target))
835 (goto-char (point-min)) 838 (goto-char (point-min))
836 (let ((outgoing-base (get-line))) 839 (while (not (eobp))
837 (unless (string-empty-p outgoing-base) 840 (push branch (alist-get (get-line) merge-bases
838 (throw 'ret outgoing-base)))))))))) 841 nil nil #'equal))
842 (forward-line 1))))
843 (erase-buffer)
844 (unless (apply #'vc-git--out-ok "merge-base" "--independent"
845 (mapcar #'car merge-bases))
846 (error "`git merge-base --independent' failed"))
847 ;; If 'git merge-base --independent' printed more than one
848 ;; line, just pick the first.
849 (goto-char (point-min))
850 (catch 'ret
851 (dolist (target (cdr (assoc (get-line) merge-bases)))
852 (erase-buffer)
853 (when (vc-git--out-ok "for-each-ref"
854 "--format=%(upstream:short)"
855 (concat "refs/heads/" target))
856 (goto-char (point-min))
857 (let ((outgoing-base (get-line)))
858 (unless (string-empty-p outgoing-base)
859 (throw 'ret outgoing-base)))))))))))
839 860
840(defun vc-git-dir--branch-headers () 861(defun vc-git-dir--branch-headers ()
841 "Return headers for branch-related information." 862 "Return headers for branch-related information."
@@ -1451,7 +1472,9 @@ line of the commit message in an entry with key \"Subject\"."
1451 (if (eq system-type 'windows-nt) 1472 (if (eq system-type 'windows-nt)
1452 locale-coding-system 1473 locale-coding-system
1453 coding-system-for-write))) 1474 coding-system-for-write)))
1454 (vc-git--call input-file t "mailinfo" msg-file patch-file)) 1475 (vc-git--call input-file t "mailinfo"
1476 (file-local-name msg-file)
1477 (file-local-name patch-file)))
1455 (goto-char (point-min)) 1478 (goto-char (point-min))
1456 ;; git-mailinfo joins up any header continuation lines for us. 1479 ;; git-mailinfo joins up any header continuation lines for us.
1457 (while (re-search-forward "^\\([^\t\n\s:]+\\):\\(.*\\)$" nil t) 1480 (while (re-search-forward "^\\([^\t\n\s:]+\\):\\(.*\\)$" nil t)
@@ -1591,7 +1614,9 @@ If PROMPT is non-nil, prompt for the Git command to run."
1591 (vc-filter-command-function 1614 (vc-filter-command-function
1592 (if prompt 1615 (if prompt
1593 (lambda (&rest args) 1616 (lambda (&rest args)
1594 (cl-destructuring-bind (&whole args git _ flags) 1617 (cl-destructuring-bind
1618 (&whole args git _ flags
1619 &aux (vc-user-edit-command-history 'vc-git-history))
1595 (apply #'vc-user-edit-command args) 1620 (apply #'vc-user-edit-command args)
1596 (setq git-program git 1621 (setq git-program git
1597 command (car flags) 1622 command (car flags)
@@ -2567,9 +2592,9 @@ This command shares argument histories with \\[rgrep] and \\[grep]."
2567 ;; In *vc-dir*, if nothing is marked, act on the whole working tree 2592 ;; In *vc-dir*, if nothing is marked, act on the whole working tree
2568 ;; regardless of the position of point. This preserves historical 2593 ;; regardless of the position of point. This preserves historical
2569 ;; behavior and is also probably more useful. 2594 ;; behavior and is also probably more useful.
2570 (if (derived-mode-p 'vc-dir-mode) 2595 (mapcar #'file-relative-name (if (derived-mode-p 'vc-dir-mode)
2571 (vc-dir-marked-files) 2596 (vc-dir-marked-files)
2572 (cadr (vc-deduce-fileset)))) 2597 (cadr (vc-deduce-fileset)))))
2573 2598
2574(defun vc-git-stash (name) 2599(defun vc-git-stash (name)
2575 "Create a stash named NAME. 2600 "Create a stash named NAME.
diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el
index 770906ff6cc..88324a2a444 100644
--- a/lisp/vc/vc.el
+++ b/lisp/vc/vc.el
@@ -3330,15 +3330,13 @@ to which `vc-push' would push as UPSTREAM-LOCATION, unconditionally.
3330(This is passed when the user invokes an outgoing base command with a 3330(This is passed when the user invokes an outgoing base command with a
3331 \\`C-u C-u' prefix argument; see `vc--maybe-read-outgoing-base'.) 3331 \\`C-u C-u' prefix argument; see `vc--maybe-read-outgoing-base'.)
3332REFRESH is passed on to `vc--incoming-revision'." 3332REFRESH is passed on to `vc--incoming-revision'."
3333 (if-let* ((incoming 3333 (vc-call-backend backend 'mergebase
3334 (vc--incoming-revision backend 3334 (vc--incoming-revision backend
3335 (pcase upstream-location 3335 (pcase upstream-location
3336 ('t nil) 3336 ('t nil)
3337 ('nil (vc--outgoing-base backend)) 3337 ('nil (vc--outgoing-base backend))
3338 (_ upstream-location)) 3338 (_ upstream-location))
3339 refresh))) 3339 refresh)))
3340 (vc-call-backend backend 'mergebase incoming)
3341 (user-error "No incoming revision -- local-only branch?")))
3342 3340
3343;;;###autoload 3341;;;###autoload
3344(defun vc-root-diff-outgoing-base (&optional upstream-location) 3342(defun vc-root-diff-outgoing-base (&optional upstream-location)
@@ -3349,7 +3347,9 @@ Uncommitted changes are included in the diff.
3349 3347
3350When unspecified, UPSTREAM-LOCATION is the outgoing base. 3348When unspecified, UPSTREAM-LOCATION is the outgoing base.
3351For a trunk branch this is always the place \\[vc-push] would push to. 3349For a trunk branch this is always the place \\[vc-push] would push to.
3352For a topic branch, query the backend for an appropriate outgoing base. 3350For a topic branch, see whether the branch matches one of
3351`vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query
3352the backend for an appropriate outgoing base.
3353See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding 3353See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding
3354the difference between trunk and topic branches. 3354the difference between trunk and topic branches.
3355 3355
@@ -3377,7 +3377,9 @@ Uncommitted changes are included in the diff.
3377 3377
3378When unspecified, UPSTREAM-LOCATION is the outgoing base. 3378When unspecified, UPSTREAM-LOCATION is the outgoing base.
3379For a trunk branch this is always the place \\[vc-push] would push to. 3379For a trunk branch this is always the place \\[vc-push] would push to.
3380For a topic branch, query the backend for an appropriate outgoing base. 3380For a topic branch, see whether the branch matches one of
3381`vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query
3382the backend for an appropriate outgoing base.
3381See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding 3383See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding
3382the difference between trunk and topic branches. 3384the difference between trunk and topic branches.
3383 3385
@@ -3411,7 +3413,9 @@ working revision and UPSTREAM-LOCATION.
3411 3413
3412When unspecified, UPSTREAM-LOCATION is the outgoing base. 3414When unspecified, UPSTREAM-LOCATION is the outgoing base.
3413For a trunk branch this is always the place \\[vc-push] would push to. 3415For a trunk branch this is always the place \\[vc-push] would push to.
3414For a topic branch, query the backend for an appropriate outgoing base. 3416For a topic branch, see whether the branch matches one of
3417`vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query
3418the backend for an appropriate outgoing base.
3415See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding 3419See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding
3416the difference between trunk and topic branches. 3420the difference between trunk and topic branches.
3417 3421
@@ -3443,7 +3447,9 @@ working revision and UPSTREAM-LOCATION.
3443 3447
3444When unspecified, UPSTREAM-LOCATION is the outgoing base. 3448When unspecified, UPSTREAM-LOCATION is the outgoing base.
3445For a trunk branch this is always the place \\[vc-push] would push to. 3449For a trunk branch this is always the place \\[vc-push] would push to.
3446For a topic branch, query the backend for an appropriate outgoing base. 3450For a topic branch, see whether the branch matches one of
3451`vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query
3452the backend for an appropriate outgoing base.
3447See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding 3453See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding
3448the difference between trunk and topic branches. 3454the difference between trunk and topic branches.
3449 3455
@@ -4435,20 +4441,23 @@ BACKEND is the VC backend."
4435 ;; Do store `nil', before signaling an error, if there is no incoming 4441 ;; Do store `nil', before signaling an error, if there is no incoming
4436 ;; revision, because that's also something that can be slow to 4442 ;; revision, because that's also something that can be slow to
4437 ;; determine and so should be remembered. 4443 ;; determine and so should be remembered.
4438 (if-let* ((_ (not refresh)) 4444 (or (if-let* ((_ (not refresh))
4439 (record (assoc upstream-location 4445 (record (assoc upstream-location
4440 (vc--repo-getprop backend 'vc-incoming-revision)))) 4446 (vc--repo-getprop backend
4441 (cdr record) 4447 'vc-incoming-revision))))
4442 (let ((res (vc-call-backend backend 'incoming-revision 4448 (cdr record)
4443 upstream-location refresh))) 4449 (let ((res (vc-call-backend backend 'incoming-revision
4444 (if-let* ((alist (vc--repo-getprop backend 'vc-incoming-revision))) 4450 upstream-location refresh)))
4445 (setf (alist-get upstream-location alist nil nil #'equal) 4451 (if-let* ((alist (vc--repo-getprop backend
4446 res) 4452 'vc-incoming-revision)))
4447 (vc--repo-setprop backend 4453 (setf (alist-get upstream-location alist
4448 'vc-incoming-revision 4454 nil nil #'equal)
4449 `((,upstream-location . ,res)))) 4455 res)
4450 (or res 4456 (vc--repo-setprop backend
4451 (user-error "No incoming revision -- local-only branch?"))))) 4457 'vc-incoming-revision
4458 `((,upstream-location . ,res))))
4459 res))
4460 (user-error "No incoming revision -- local-only branch?")))
4452 4461
4453;;;###autoload 4462;;;###autoload
4454(defun vc-root-log-incoming (&optional upstream-location) 4463(defun vc-root-log-incoming (&optional upstream-location)
@@ -5017,6 +5026,9 @@ log entries should be gathered."
5017 5026
5018(defvar vc-filter-command-function) 5027(defvar vc-filter-command-function)
5019 5028
5029(defvar vc-edit-next-command-history nil
5030 "Minibuffer history for `vc-edit-next-command'.")
5031
5020;;;###autoload 5032;;;###autoload
5021(defun vc-edit-next-command () 5033(defun vc-edit-next-command ()
5022 "Request editing the next VC shell command before execution. 5034 "Request editing the next VC shell command before execution.
@@ -5040,7 +5052,8 @@ immediately after this one."
5040 (add-hook 'prefix-command-echo-keystrokes-functions echofun) 5052 (add-hook 'prefix-command-echo-keystrokes-functions echofun)
5041 (setq vc-filter-command-function 5053 (setq vc-filter-command-function
5042 (lambda (&rest args) 5054 (lambda (&rest args)
5043 (apply #'vc-user-edit-command (apply old args)))))) 5055 (let ((vc-user-edit-command-history 'vc-edit-next-command-history))
5056 (apply #'vc-user-edit-command (apply old args)))))))
5044 5057
5045;; This is used in .dir-locals.el in the Emacs source tree. 5058;; This is used in .dir-locals.el in the Emacs source tree.
5046;;;###autoload (put 'vc-prepare-patches-separately 'safe-local-variable 'booleanp) 5059;;;###autoload (put 'vc-prepare-patches-separately 'safe-local-variable 'booleanp)
diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index 6d576a10b73..353d546fce4 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -1334,10 +1334,10 @@ POS defaults to the value of (point). If user option
1334This is much faster.") 1334This is much faster.")
1335 1335
1336(defun widget-move (arg &optional suppress-echo) 1336(defun widget-move (arg &optional suppress-echo)
1337 "Move point to the ARG next field or button. 1337 "Move point to the ARGth next field or button.
1338ARG may be negative to move backward. 1338ARG may be negative to move backward.
1339When the second optional argument is non-nil, 1339If the optional argument SUPPRESS-ECHO is non-nil, suppress showing
1340nothing is shown in the echo area." 1340in the echo area the help-echo, if any, for the final position."
1341 (let* ((wrapped 0) 1341 (let* ((wrapped 0)
1342 (number arg) 1342 (number arg)
1343 (fwd (> arg 0)) ; widget-forward is caller. 1343 (fwd (> arg 0)) ; widget-forward is caller.
@@ -1384,19 +1384,19 @@ nothing is shown in the echo area."
1384 (run-hooks 'widget-move-hook)) 1384 (run-hooks 'widget-move-hook))
1385 1385
1386(defun widget-forward (arg &optional suppress-echo) 1386(defun widget-forward (arg &optional suppress-echo)
1387 "Move point to the next field or button. 1387 "Move point forward across ARG fields or buttons.
1388With optional ARG, move across that many fields. 1388Interactively, ARG is the prefix numeric argument and defaults to 1.
1389When the second optional argument is non-nil, 1389If the optional argument SUPPRESS-ECHO is non-nil, suppress showing
1390nothing is shown in the echo area." 1390in the echo area the help-echo, if any, for the final position."
1391 (interactive "p") 1391 (interactive "p")
1392 (run-hooks 'widget-forward-hook) 1392 (run-hooks 'widget-forward-hook)
1393 (widget-move arg suppress-echo)) 1393 (widget-move arg suppress-echo))
1394 1394
1395(defun widget-backward (arg &optional suppress-echo) 1395(defun widget-backward (arg &optional suppress-echo)
1396 "Move point to the previous field or button. 1396 "Move point back across ARG fields or buttons.
1397With optional ARG, move across that many fields. 1397Interactively, ARG is the prefix numeric argument and defaults to 1.
1398When the second optional argument is non-nil, 1398If the optional argument SUPPRESS-ECHO is non-nil, suppress showing
1399nothing is shown in the echo area." 1399in the echo area the help-echo, if any, for the final position."
1400 (interactive "p") 1400 (interactive "p")
1401 (run-hooks 'widget-backward-hook) 1401 (run-hooks 'widget-backward-hook)
1402 (widget-move (- arg) suppress-echo)) 1402 (widget-move (- arg) suppress-echo))
diff --git a/lisp/window.el b/lisp/window.el
index 3a1ebd16fa6..2327ffcd5f2 100644
--- a/lisp/window.el
+++ b/lisp/window.el
@@ -7586,7 +7586,7 @@ strategy."
7586 7586
7587(defun window--frame-landscape-p (&optional frame) 7587(defun window--frame-landscape-p (&optional frame)
7588 "Non-nil if FRAME is wider than it is tall. 7588 "Non-nil if FRAME is wider than it is tall.
7589This means actually wider on the screen, not character-wise. 7589This means actually wider on the screen, not wider character-wise.
7590On text frames, use the heuristic that characters are roughtly twice as 7590On text frames, use the heuristic that characters are roughtly twice as
7591tall as they are wide." 7591tall as they are wide."
7592 (if (display-graphic-p frame) 7592 (if (display-graphic-p frame)