diff options
| author | Helmut Eller | 2026-02-13 09:10:16 +0100 |
|---|---|---|
| committer | Helmut Eller | 2026-02-13 09:10:16 +0100 |
| commit | 91c9e9883488d715a30877dfd7641ef4b3c62658 (patch) | |
| tree | e2c4525147e443f86baf9d0144aeadec082d7564 /lisp | |
| parent | 9a4a54af9192a6653164364c75721ee814ffb1e8 (diff) | |
| parent | f1fe4d46190263e164ccd1e066095d46a156297f (diff) | |
| download | emacs-feature/igc.tar.gz emacs-feature/igc.zip | |
Merge branch 'master' into feature/igcfeature/igc
Diffstat (limited to 'lisp')
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) | 211 | A value of nil means do not poll for battery status changes. |
| 212 | This can be useful when `battery-status-function' is set to | ||
| 213 | `battery-upower' and `battery-upower-subscribe' is non-nil, in | ||
| 214 | which 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'. | ||
| 785 | This is a composite device for displaying a digest of overall state. | ||
| 786 | In 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. | ||
| 794 | Each value is a string property of `battery-upower-path' | ||
| 795 | or `battery-upower-display-device-path'. | ||
| 796 | A 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. |
| 809 | Respond only to those in `battery-upower-subscribe-properties'. | ||
| 784 | Intended as a UPower PropertiesChanged signal handler." | 810 | Intended 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. |
| 83 | You probably want to make `appt-display-interval' a factor of this." | 83 | You 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'). | |||
| 92 | For example, to be warned 30 minutes in advance of an appointment: | 91 | For 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'." | |||
| 120 | This is in addition to any other display of appointment messages. | 116 | This is in addition to any other display of appointment messages. |
| 121 | The mode line updates every minute, independent of the value of | 117 | The 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. |
| 128 | Only relevant if reminders are to be displayed in their own window." | 123 | Only 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. |
| 134 | This occurs when this package is first activated, and then at | 128 | This occurs when this package is first activated, and then at |
| 135 | midnight when the appointment list updates." | 129 | midnight 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. | |||
| 146 | Note that this variable controls the interval at which | 139 | Note 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) |
| 148 | always updates every minute." | 141 | always 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 | |||
| 156 | the appointment, the current time, and the text of the appointment. | 148 | the appointment, the current time, and the text of the appointment. |
| 157 | Each argument may also be a list, if multiple appointments are | 149 | Each argument may also be a list, if multiple appointments are |
| 158 | relevant at any one time." | 150 | relevant 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. |
| 164 | Only relevant if reminders are being displayed in a window." | 155 | Only 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. |
| 66 | Default is for Beijing. This is an expression in `year' since it changed at | 66 | Default is for Beijing. This is an expression in `year' since it changed at |
| 67 | 1928-01-01 00:00:00 from UT+7:45:40 to UT+8." | 67 | 1928-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. |
| 86 | Default is for no daylight saving time." | 84 | Default 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." | |||
| 95 | This is an expression depending on `year' because it changed | 92 | This is an expression depending on `year' because it changed |
| 96 | at 1928-01-01 00:00:00 from `PMT' to `CST'." | 93 | at 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'." | |||
| 113 | Default is for no daylight saving time. See documentation of | 108 | Default 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 | |||
| 124 | Default is for no daylight saving time. See documentation of | 118 | Default 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. |
| 132 | Default is for no daylight saving time." | 125 | Default 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. |
| 138 | Default is for no daylight saving time." | 130 | Default 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 | |||
| 46 | correct, since the dates of daylight saving transitions sometimes | 46 | correct, since the dates of daylight saving transitions sometimes |
| 47 | change." | 47 | change." |
| 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 | ||
| 70 | If the locale never uses daylight saving time, set this to nil." | 69 | If 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 | ||
| 87 | If the locale never uses daylight saving time, set this to nil." | 85 | If 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: | |||
| 309 | UTC-DIFF is an integer specifying the number of minutes difference between | 308 | UTC-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. |
| 312 | DST-OFFSET is an integer giving the daylight saving time offset in minutes. | 311 | DST-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.) | ||
| 313 | STD-ZONE is a string giving the name of the time zone when no seasonal time | 314 | STD-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. |
| 315 | DST-ZONE is a string giving the name of the time zone when there is a seasonal | 316 | DST-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. |
| 341 | For example, -300 for New York City, -480 for Los Angeles." | 342 | For 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. |
| 348 | If the locale never uses daylight saving time, set this to 0." | 348 | If 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." | |||
| 360 | For example, \"-0500\" or \"EST\" in New York City." | 359 | For 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." | |||
| 374 | For example, \"-0400\" or \"EDT\" in New York City." | 372 | For 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'." | |||
| 421 | Fractional part of DATE is local standard time of day." | 416 | Fractional 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." | |||
| 317 | Characters are replaced according to `cal-html-html-subst-list'." | 311 | Characters 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. |
| 73 | Sunday is 0, Monday is 1, and so on. The default is to print from Sunday to | 73 | Sunday is 0, Monday is 1, and so on. The default is to print from Sunday to |
| 74 | Saturday. For example, (1 3 5) prints only Monday, Wednesday, Friday." | 74 | Saturday. 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. |
| 80 | Setting this to nil may speed up calendar generation." | 79 | Setting 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. |
| 86 | Setting this to nil may speed up calendar generation." | 84 | Setting 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. |
| 92 | At present, this only affects the daily filofax calendar." | 89 | At 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 | ||
| 114 | will put the Hebrew date at the bottom of each day." | 110 | will 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. |
| 130 | At present, this only affects `cal-tex-cursor-day'." | 123 | At 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. |
| 136 | At present, this only affects `cal-tex-cursor-day'." | 128 | At 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 | |||
| 153 | characters with diacritical marks to their LaTeX equivalents, use | 143 | characters 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 | |||
| 871 | a portion of the first word of the diary entry. | 871 | a portion of the first word of the diary entry. |
| 872 | 872 | ||
| 873 | For examples of three common styles, see `diary-american-date-forms', | 873 | For 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 | |||
| 876 | If you customize this variable, you should also customize the variable | ||
| 877 | `diary-date-insertion-form' to contain a pseudo-pattern which produces | ||
| 878 | dates that match one of the forms in this variable. (If | ||
| 879 | `diary-date-insertion-form' does not correspond to one of the patterns | ||
| 880 | in this variable, then the diary will not recognize such dates, | ||
| 881 | including 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 | |||
| 921 | A pseudo-pattern is a list of expressions that can include the symbols | ||
| 922 | `month', `day', and `year' (all numbers in string form), and `monthname' | ||
| 923 | and `dayname' (both alphabetic strings). For example, a typical American | ||
| 924 | form would be | ||
| 925 | |||
| 926 | (month \"/\" day \"/\" (substring year -2)) | ||
| 927 | |||
| 928 | whereas | ||
| 929 | |||
| 930 | ((format \"%9s, %9s %2s, %4s\" dayname monthname day year)) | ||
| 931 | |||
| 932 | would give the usual American style in fixed-length fields. | ||
| 933 | |||
| 934 | This pattern will be used by `calendar-date-string' (which see) to | ||
| 935 | format dates when inserting them with `diary-insert-entry', or when | ||
| 936 | importing them from other formats into the diary. | ||
| 937 | |||
| 938 | If you customize this variable, you should also customize the variable | ||
| 939 | `diary-date-forms' to include a pseudo-pattern which matches dates | ||
| 940 | produced by this pattern. (If there is no corresponding pattern in | ||
| 941 | `diary-date-forms', then the diary will not recognize such dates, | ||
| 942 | including 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. | ||
| 1357 | DAYNO 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 | ||
| 65 | accept an optional argument, QUIETLY, which determines whether these | ||
| 66 | functions ask for confirmation when importing individual events and | ||
| 67 | saving the diary file. If you set this variable to t, you will never be | ||
| 68 | asked 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 | |||
| 76 | The functions in this hook will be run in a temporary buffer after | ||
| 77 | formatting the contents of iCalendar data as diary entries in that | ||
| 78 | buffer. You can add functions to this hook if you want, for example, to | ||
| 79 | copy 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 | |||
| 86 | If the value is nil, binary attachments encoded in an ATTACH property | ||
| 87 | are never saved. If it is the name of a directory, attachments will be | ||
| 88 | saved in per-component subdirectories of this directory, with each | ||
| 89 | subdirectory 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 | |||
| 98 | The value must be a valid format string for `format-time-string'; see | ||
| 99 | its docstring for more information. The value only needs to format clock | ||
| 100 | times, and should format them in a way that will be recognized by | ||
| 101 | `diary-time-regexp'. (Date information is formatted separately at the | ||
| 102 | start 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 | |||
| 114 | This should be a function which inserts information about an | ||
| 115 | `icalendar-attendee' into the current buffer. It is convenient to | ||
| 116 | express such a function as a skeleton; see `define-skeleton' and | ||
| 117 | `skeleton-insert' for more information. | ||
| 118 | |||
| 119 | The function will be called with one argument, ATTENDEE, which will be | ||
| 120 | an `icalendar-attendee' syntax node. It should insert information about | ||
| 121 | the attendee into the current buffer. See `icalendar-with-property' for | ||
| 122 | a convenient way to bind the data in ATTENDEE. | ||
| 123 | |||
| 124 | For convenience when writing this function as a skeleton, the following | ||
| 125 | variables will also be (dynamically) bound when the function is called. | ||
| 126 | All 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 | |||
| 142 | This regular expression should match calendar addresses (which are | ||
| 143 | typically \"mailto:\" URIs) which should be skipped when importing | ||
| 144 | ATTENDEE, ORGANIZER, and other iCalendar properties that identify a | ||
| 145 | contact. | ||
| 146 | |||
| 147 | You can make this match your own email address(es) to prevent them from | ||
| 148 | being formatted by `diary-icalendar-attendee-format-function' and | ||
| 149 | listed 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 | |||
| 156 | This function is called with one argument VEVENT, an `icalendar-vevent'. | ||
| 157 | It should insert formatted data from this event into the current buffer. | ||
| 158 | It 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 | ||
| 161 | VEVENT. | ||
| 162 | |||
| 163 | For convenience when writing this function as a skeleton, the following | ||
| 164 | variables will be (dynamically) bound when the function is called. All | ||
| 165 | values 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 | |||
| 217 | This function is called with one argument VJOURNAL, an | ||
| 218 | `icalendar-vjournal'. It should insert formatted data from this journal | ||
| 219 | entry into the current buffer. It is convenient to express such a | ||
| 220 | function as a skeleton; see `define-skeleton' and `skeleton-insert' for | ||
| 221 | more information, and see `diary-icalendar-vjournal-skeleton' for an | ||
| 222 | example. See `icalendar-with-component' for a convenient way to bind | ||
| 223 | the data in VJOURNAL. | ||
| 224 | |||
| 225 | For convenience when writing this function as a skeleton, the following | ||
| 226 | variables will be (dynamically) bound when the function is called. All | ||
| 227 | values 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 | |||
| 264 | If this variable is non-nil, VJOURNAL components will be imported into | ||
| 265 | the diary as \"nonmarking\" entries by prefixing | ||
| 266 | `diary-nonmarking-symbol'. This means they will not cause their date to | ||
| 267 | be marked in the calendar when the command `diary-mark-entries' is | ||
| 268 | called. See Info node `(emacs)Displaying the Diary' for more | ||
| 269 | information." | ||
| 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 | |||
| 277 | This function is called with one argument VTODO, an `icalendar-vtodo'. | ||
| 278 | It should insert formatted data from this task into the current buffer. | ||
| 279 | It 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 | ||
| 282 | VTODO. | ||
| 283 | |||
| 284 | For convenience when writing this function as a skeleton, the following | ||
| 285 | variables will be (dynamically) bound when the function is called. All | ||
| 286 | values 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 | |||
| 347 | This function must accept one argument, which will be an | ||
| 348 | `icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal' | ||
| 349 | component. It should return non-nil if this component should be | ||
| 350 | formatted for import, or nil if it should be skipped. | ||
| 351 | |||
| 352 | The default value will format all the events, todos, and journal entries | ||
| 353 | in 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 | |||
| 378 | The full address should match group 1; \"mailto:\" will be prepended to | ||
| 379 | the full address during export, unless it or another URI scheme is | ||
| 380 | present. If there is a match in group 2, it will be used as the | ||
| 381 | common name associated with the address (see `icalendar-cnparam'). | ||
| 382 | |||
| 383 | The default value matches names and addresses on lines like: | ||
| 384 | |||
| 385 | Ms. Baz <baz@foo.com> | ||
| 386 | |||
| 387 | as well as on lines like: | ||
| 388 | |||
| 389 | Property: Ms. Baz <baz@foo.com> other data... | ||
| 390 | |||
| 391 | Any matching address within a diary entry will be exported as an | ||
| 392 | iCalendar ATTENDEE property, unless the line on which it appears is also | ||
| 393 | a match for `diary-icalendar-organizer-regexp', in which case it will be | ||
| 394 | exported 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 | |||
| 401 | If this is nil, the entire entry (after the date and time specification) | ||
| 402 | is used as the description. Thus, it is only necessary to set this | ||
| 403 | variable if you want to export diary entries where the text to be used | ||
| 404 | as the description should not include the full entry body. In that case, | ||
| 405 | the 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 | |||
| 417 | This regular expression need *not* match the name and address of the | ||
| 418 | organizer (`diary-icalendar-address-regexp' is responsible for that). | ||
| 419 | It only needs to match a line on which the organizer's address appears, | ||
| 420 | to 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 | |||
| 433 | The access classification value should be matched by group 1. The default | ||
| 434 | regexp matches access classifications like: | ||
| 435 | Access: C | ||
| 436 | or | ||
| 437 | Class: C | ||
| 438 | where 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 | |||
| 453 | The location value should be matched by group 1. The default regexp | ||
| 454 | matches 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 | |||
| 469 | The status value should be matched by group 1. The default regexp | ||
| 470 | matches statuses on lines like: | ||
| 471 | |||
| 472 | Status: S | ||
| 473 | |||
| 474 | where 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 | |||
| 490 | If this is nil, the first line of the entry (after the date and time | ||
| 491 | specification) is used as the summary. Thus, it is only necessary to set | ||
| 492 | this variable if you want to export diary entries where the text to be | ||
| 493 | used as the summary does not appear on the first line of the entry. In | ||
| 494 | that 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 | |||
| 501 | If this is non-nil, any diary entry that matches this regexp will be | ||
| 502 | exported as an iCalendar VTODO component (instead of VEVENT), with its | ||
| 503 | due 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 | |||
| 516 | The UID value should be matched by group 1. The default regexp matches | ||
| 517 | UIDs 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 | |||
| 531 | The full URL should be matched by group 1. The default regexp matches | ||
| 532 | URLs 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 | |||
| 541 | If this variable is nil, nonmarking diary entries (those prefixed with | ||
| 542 | `diary-nonmarking-symbol') are never exported. If it is non-nil, | ||
| 543 | nonmarking diary entries are exported; see also | ||
| 544 | `diary-icalendar-export-nonmarking-as-vjournal' for more control over | ||
| 545 | how 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 | |||
| 553 | If this variable is non-nil, nonmarking diary entries (those prefixed | ||
| 554 | with `diary-nonmarking-symbol') will be exported as iCalendar VJOURNAL | ||
| 555 | components, rather than VEVENT components. VJOURNAL components are | ||
| 556 | intended to represent notes, documents, or other data associated with a | ||
| 557 | date. External calendar applications may treat VJOURNAL components | ||
| 558 | differently than VEVENTs, so consult your application's documentation | ||
| 559 | before setting this variable to t. | ||
| 560 | |||
| 561 | If this variable is nil, nonmarking entries will be exported as VEVENT | ||
| 562 | components which do not take up busy time in the calendar (i.e., with | ||
| 563 | the 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 | |||
| 573 | If this variable is nil, no alarms are created during export. | ||
| 574 | If it is non-nil, it should be a list of lists like: | ||
| 575 | |||
| 576 | \((TYPE LEAD-TIME [OPTIONS]) ...) | ||
| 577 | |||
| 578 | In each inner list, the first element TYPE should be a symbol indicating | ||
| 579 | an alarm type to generate: one of \\='audio, \\='display, or \\='email. | ||
| 580 | The second element LEAD-TIME should be an integer specifying the amount | ||
| 581 | of time before the event, in minutes, when the alarm should be | ||
| 582 | triggered. For audio alarms, there are currently no other | ||
| 583 | OPTIONS. | ||
| 584 | |||
| 585 | For display and email alarms, the next OPTION is a format string for the | ||
| 586 | displayed alarm, or the email subject line. In this string, \"%t\" will | ||
| 587 | be replaced with LEAD-TIME and \"%s\" with the event's summary. | ||
| 588 | |||
| 589 | If TYPE is \\='email, the next OPTION should be a list whose members | ||
| 590 | specify the email addresses to which email alarms should be sent. These | ||
| 591 | can either be email addresses (as strings), or the symbol | ||
| 592 | \\='from-entry, meaning that these addresses should be taken from the | ||
| 593 | exported 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 | |||
| 633 | Some S-expression entries cannot be translated to iCalendar format. | ||
| 634 | They are therefore enumerated, i.e., explicitly evaluated for a | ||
| 635 | certain number of days, and then exported. The enumeration starts | ||
| 636 | on the current day and continues for the number of days given here. | ||
| 637 | |||
| 638 | See `icalendar-export-sexp-enumerate-all' for a list of sexp | ||
| 639 | entries 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 | |||
| 647 | If this variable is non-nil, all S-expression diary entries are | ||
| 648 | enumerated for `diary-icalendar-export-sexp-enumeration-days' days | ||
| 649 | instead of translating them into an iCalendar equivalent. | ||
| 650 | This causes the following S-expression entries to be enumerated | ||
| 651 | instead 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' | ||
| 660 | All 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 | |||
| 669 | Set this to a year just before the start of your personal calendar. | ||
| 670 | It is needed when exporting certain diary S-expressions to iCalendar | ||
| 671 | recurring events, and because some calendar browsers only propagate | ||
| 672 | recurring 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 | |||
| 680 | The symbol `local' (the default) means to assume that times are in the | ||
| 681 | time zone determined by `calendar-current-time-zone'. The time zone | ||
| 682 | information returned by that function will be exported as an iCalendar | ||
| 683 | VTIMEZONE component, and clock times in the diary file will be exported | ||
| 684 | with a reference to that time zone definition. | ||
| 685 | |||
| 686 | On some systems, `calendar-current-time-zone' cannot determine time zone | ||
| 687 | information for the local time zone. In that case, you can set this | ||
| 688 | variable 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 | |||
| 693 | This list describes the time zone you would like to use for export. See | ||
| 694 | the docstring of `calendar-current-time-zone' for details. Times in the | ||
| 695 | diary file will be exported like with `local' for this time zone. | ||
| 696 | |||
| 697 | The other possible values for this variable avoid the need to include | ||
| 698 | any time zone information in the exported iCalendar data: | ||
| 699 | |||
| 700 | The symbol `to-utc' means to re-encode all exported times to UTC | ||
| 701 | time. In this case, export will assume that times are in Emacs local | ||
| 702 | time, and rely on `encode-time' and `decode-time' to convert them to UTC | ||
| 703 | times. | ||
| 704 | |||
| 705 | The symbol `floating' means to export clock times without any time | ||
| 706 | zone identifier, which the iCalendar standard (RFC5545) calls | ||
| 707 | \"floating\" times. RFC5545 specifies that floating times should be | ||
| 708 | interpreted as local to whichever time zone the recipient of the | ||
| 709 | iCalendar data is currently in (which might be different from your local | ||
| 710 | time zone). You should only use this if that behavior makes sense for | ||
| 711 | the 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 | |||
| 727 | If this is non-nil, each line of a diary entry will be exported as a | ||
| 728 | separate iCalendar event. | ||
| 729 | |||
| 730 | If you write your diary entries in a one-entry-per-day style, with | ||
| 731 | multiple events or appointments per day, you can use this variable to | ||
| 732 | export these individual events to iCalendar format. For example, an | ||
| 733 | entry like: | ||
| 734 | |||
| 735 | 2025-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 | |||
| 742 | will be exported as four events, each on 2025-05-03 but with different | ||
| 743 | start times (except for the second event, \"Start experiment A\", which | ||
| 744 | has no start time). An event line can be continued onto subsequent lines | ||
| 745 | via additional indentation, as in the first event in this entry. | ||
| 746 | |||
| 747 | If this variable is non-nil, each distinct event must begin on a | ||
| 748 | continuation line of the entry (below the date); any text on the same | ||
| 749 | line as the date is ignored. A time specification can only appear at | ||
| 750 | the beginning of each continuation line of the entry, immediately after | ||
| 751 | the leading whitespace. | ||
| 752 | |||
| 753 | If this variable is nil, each entry will be exported as exactly one | ||
| 754 | event, and only a time specification immediately following the date will | ||
| 755 | determine the start and end times for that event. Thus, in the example | ||
| 756 | above, the exported event would have a start date but no start time or | ||
| 757 | end time. The times in the entry would be preserved as text in the | ||
| 758 | event 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 | |||
| 766 | If you like to keep your diary entries in a particular format, you can | ||
| 767 | set this to a function which parses that format to iCalendar properties | ||
| 768 | during iCalendar export, so that other calendar applications can use | ||
| 769 | them. | ||
| 770 | |||
| 771 | The parsing function will be called with the current restriction set to | ||
| 772 | the boundaries of a diary entry. If `diary-icalendar-export-linewise' | ||
| 773 | is non-nil, the restriction will correspond to a single event in a | ||
| 774 | multi-line diary entry. | ||
| 775 | |||
| 776 | The function should accept two arguments, TYPE and PROPERTIES. TYPE is | ||
| 777 | the iCalendar type symbol (one of \\='icalendar-vevent, | ||
| 778 | \\='icalendar-vjournal, or \\='icalendar-vtodo) for the component being | ||
| 779 | generated for the entry. PROPERTIES is the list of property nodes that | ||
| 780 | `diary-icalendar-parse-entry' has already parsed from the entry and will | ||
| 781 | be included in the exported component. | ||
| 782 | |||
| 783 | The 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 | ||
| 786 | node created from the current entry. See the docstrings of those | ||
| 787 | symbols for more information on the properties they can contain, and the | ||
| 788 | `icalendar-make-property' macro for a simple way to create property | ||
| 789 | nodes 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. | ||
| 836 | If any of these variables have non-default values, they will be used by | ||
| 837 | `diary-icalendar-import-format-entry' to import events. This function | ||
| 838 | is 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. | ||
| 883 | This function is for backward compatibility; please do not rely on it in | ||
| 884 | new 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. | ||
| 927 | This function is for backward compatibility; please do not rely on it in | ||
| 928 | new 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 | |||
| 976 | Group 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 | ||
| 978 | line. 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. | ||
| 983 | If 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 | |||
| 1009 | The UID must occur on a line matching `diary-icalendar-uid-regexp'. If | ||
| 1010 | such an entry exists, return markers (START END) bounding it. | ||
| 1011 | Otherwise, 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. | ||
| 1039 | Adds 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 | |||
| 1080 | Includes 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 | |||
| 1085 | The result looks like: | ||
| 1086 | <foo@example.com> | ||
| 1087 | or | ||
| 1088 | Baz Foo <foo@example.com> | ||
| 1089 | or | ||
| 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 | |||
| 1107 | ATTENDEE should be an `icalendar-attendee' or `icalendar-organizer' | ||
| 1108 | property node. Returns a string representing an entry for the attendee, | ||
| 1109 | formatted by `diary-icalendar-attendee-format-function', unless the | ||
| 1110 | attendee'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 | |||
| 1235 | The file will be named based on a unique prefix of BASE64-DATA with an | ||
| 1236 | extension based on MIMETYPE. It will be saved in a subdirectory named | ||
| 1237 | DIR of `diary-icalendar-attachment-directory', which will be created if | ||
| 1238 | necessary. 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 | |||
| 1273 | If these nodes contain binary data, rather than an URL, save the data to | ||
| 1274 | a file in `diary-icalendar-attachment-directory' (unless this variable | ||
| 1275 | is nil). UID should be the universal ID of the component containing | ||
| 1276 | ATTACHMENT-NODES; the attachments will be saved in a subdirectory of the | ||
| 1277 | same name. The returned list is a list of strings, which are either | ||
| 1278 | URLs 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 | |||
| 1295 | VALUES should be a list of strings. nil elements will be ignored, and an | ||
| 1296 | empty list will return nil. | ||
| 1297 | |||
| 1298 | TITLE is a string to add to the beginning of the list; a colon will be | ||
| 1299 | appended. PLURAL-FORM is the plural of TITLE, to be used when VALUES | ||
| 1300 | contains more than one element (default: TITLE+\"s\"). | ||
| 1301 | |||
| 1302 | The 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 | ||
| 1305 | title on its own line at the beginning, and the whole list indented | ||
| 1306 | relative to the title by INDENT spaces (default: 2). Thus, in the first | ||
| 1307 | case, the result looks like: | ||
| 1308 | TITLE(s): VAL1, VAL2, ... | ||
| 1309 | and 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. | ||
| 1337 | The time is formatted according to `diary-icalendar-time-format', which see. | ||
| 1338 | TZNAME, if specified, should be a string naming the time zone observance | ||
| 1339 | in 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 | |||
| 1349 | DT is translated to the system local time zone if necessary, and the | ||
| 1350 | original time specification is preserved in parentheses if it was given | ||
| 1351 | in a different zone. ORIGINAL-TZNAME, if specified, should be a string | ||
| 1352 | naming the time zone observance in which DT was originally encoded in | ||
| 1353 | the 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. | ||
| 1370 | If DT is a date-time, only the date part is considered. The date is | ||
| 1371 | formatted 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 | |||
| 1381 | If DT is a plain date, only the date will be formatted. If DT is a | ||
| 1382 | date-time, both the date and the time will formatted, after translating | ||
| 1383 | DT into a date and time into the system local time. | ||
| 1384 | |||
| 1385 | If specified, ORIGINAL-TZNAME should be a string naming the time zone | ||
| 1386 | observance in which DT was originally encoded in the iCalendar data. In | ||
| 1387 | this case, the original clock time in DT will also be added in | ||
| 1388 | parentheses, with date if necessary. For example: | ||
| 1389 | 2025/05/01 09:00 (08:00 GMT) | ||
| 1390 | or | ||
| 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 | |||
| 1410 | START and END should be `icalendar-date-time' values where the date part | ||
| 1411 | is 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 | ||
| 1413 | range instead.) | ||
| 1414 | |||
| 1415 | The date is only formatted once, and the time is formatted as a range, like: | ||
| 1416 | STARTDATE STARTTIME-ENDTIME | ||
| 1417 | If 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 | |||
| 1428 | START and END may be `icalendar-date' or `icalendar-date-time' | ||
| 1429 | values. If they are date-times, only the date parts will be considered. | ||
| 1430 | Returns a string like \"%%(diary-block ...)\" with the arguments properly | ||
| 1431 | ordered 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 | |||
| 1471 | The returned string looks like \"%%(diary-rrule ...)\", and contains the | ||
| 1472 | necessary data from COMPONENT for the calendar to compute recurrences of | ||
| 1473 | the 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 | |||
| 1530 | COMPONENT should be an `icalendar-vevent', `icalendar-vtodo', or | ||
| 1531 | `icalendar-vjournal'. INDEX should be an index into the calendar where | ||
| 1532 | COMPONENT occurs, as returned by `icalendar-parse-and-index'. | ||
| 1533 | |||
| 1534 | Depending 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' | ||
| 1538 | which see. | ||
| 1539 | |||
| 1540 | The variable `ical-nonmarking' will be bound to the value of NONMARKING in | ||
| 1541 | the relevant skeleton command. If it is non-nil, the user requested the | ||
| 1542 | entry to be nonmarking. | ||
| 1543 | |||
| 1544 | Returns 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 | |||
| 1778 | The 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 | ||
| 1780 | called in a (narrowed) buffer whose contents represent a single diary | ||
| 1781 | entry.") | ||
| 1782 | |||
| 1783 | (defvar di:post-calendar-format-hook nil | ||
| 1784 | "Hook run after formatting a complete `icalendar-vcalendar' as diary entries. | ||
| 1785 | |||
| 1786 | The 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 | ||
| 1788 | calendar. Each function will be called in a buffer containing all the | ||
| 1789 | diary entries.") | ||
| 1790 | |||
| 1791 | (defun di:sort-by-start-ascending (c1 c2) | ||
| 1792 | "Sort iCalendar component C1 before C2 if C1 starts strictly before C2. | ||
| 1793 | Components 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. | ||
| 1806 | See 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 | |||
| 1814 | This function parses the first iCalendar VCALENDAR in the current buffer | ||
| 1815 | and formats its VEVENT, VJOURNAL, and VTODO components as diary entries. | ||
| 1816 | It returns a new buffer containing those diary entries. The caller | ||
| 1817 | should kill this buffer when it is no longer needed. | ||
| 1818 | |||
| 1819 | If ALL-NONMARKING is non-nil, all diary entries will be non-marking. | ||
| 1820 | |||
| 1821 | The list of components to import can be filtered by binding | ||
| 1822 | `diary-icalendar-import-predicate'. After each component is formatted as | ||
| 1823 | a diary entry, `diary-icalendar-post-entry-format-hook' is run in a (narrowed) | ||
| 1824 | buffer containing that entry. After all components have been formatted, | ||
| 1825 | `diary-icalendar-post-calendar-format-hook' is run in the (widened) buffer | ||
| 1826 | containing all the entries. | ||
| 1827 | |||
| 1828 | The formatting of imported entries depends on a number of | ||
| 1829 | user-customizable variables, including: `diary-date-forms', | ||
| 1830 | `calendar-date-style', `calendar-date-display-form' and customizations | ||
| 1831 | in 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 | |||
| 1919 | This function parses the first iCalendar VCALENDAR in the current buffer | ||
| 1920 | and imports VEVENT, VJOURNAL, and VTODO components to the diary file | ||
| 1921 | DIARY-FILENAME (default: `diary-file'). | ||
| 1922 | |||
| 1923 | For each entry, you are asked whether to add it to the diary unless | ||
| 1924 | QUIETLY is non-nil. After all entries are imported, you are also asked | ||
| 1925 | if you want to save the diary file unless QUIETLY is non-nil. When | ||
| 1926 | called interactively, you are asked if you want to confirm each entry | ||
| 1927 | individually; answer No to make QUIETLY non-nil. | ||
| 1928 | |||
| 1929 | ALL-NONMARKING determines whether all diary events are created as | ||
| 1930 | non-marking entries. When called interactively, you are asked whether | ||
| 1931 | you want to make all entries non-marking. | ||
| 1932 | |||
| 1933 | The formatting of imported entries in the diary depends on a number of | ||
| 1934 | user-customizable variables. Before running this command for the first | ||
| 1935 | time, 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' | ||
| 1941 | as 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 | |||
| 1974 | This function parses the first iCalendar VCALENDAR in FILENAME and | ||
| 1975 | imports VEVENT, VJOURNAL, and VTODO components to the diary | ||
| 1976 | DIARY-FILENAME (default: `diary-file'). | ||
| 1977 | |||
| 1978 | For each entry, you are asked whether to add it to the diary unless | ||
| 1979 | QUIETLY is non-nil. After all entries are imported, you are also asked | ||
| 1980 | if you want to save the diary file unless QUIETLY is non-nil. When | ||
| 1981 | called interactively, you are asked if you want to confirm each entry | ||
| 1982 | individually; answer No to make QUIETLY non-nil. | ||
| 1983 | |||
| 1984 | NONMARKING determines whether all diary events are created as | ||
| 1985 | non-marking entries. When called interactively, you are asked whether | ||
| 1986 | you want to make all entries non-marking. | ||
| 1987 | |||
| 1988 | The formatting of imported entries in the diary depends on a number of | ||
| 1989 | user-customizable variables. Before running this command for the first | ||
| 1990 | time, 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' | ||
| 1996 | as 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 | |||
| 2024 | This function is a suitable viewer for text/calendar parts in MIME | ||
| 2025 | messages, such as email attachments. To use this function as a viewer, | ||
| 2026 | customize the variable `mailcap-user-mime-data' and add an entry | ||
| 2027 | containing this function for the MIME type \"text/calendar\". | ||
| 2028 | |||
| 2029 | To 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 | |||
| 2066 | Searches the entry in the current restriction for addresses matching | ||
| 2067 | `diary-icalendar-address-regexp'. If an address is found on a | ||
| 2068 | line that also matches `diary-icalendar-organizer-regexp', it will be | ||
| 2069 | parsed 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 | |||
| 2094 | Searches the entry in the current restriction for a location matching | ||
| 2095 | `diary-icalendar-location-regexp'. If a location is found, it will be | ||
| 2096 | parsed as an `icalendar-location' node. Returns a list containing just | ||
| 2097 | this 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 | |||
| 2106 | Searches the entry in the current restriction for an access | ||
| 2107 | classification matching `diary-icalendar-class-regexp'. If a | ||
| 2108 | classification is found, it will be parsed as an `icalendar-class' | ||
| 2109 | node. 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 | |||
| 2119 | Searches the entry in the current restriction for a status matching | ||
| 2120 | `diary-icalendar-status-regexp'. If a status is found, it will be parsed | ||
| 2121 | as 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 | |||
| 2131 | Searches 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 | |||
| 2142 | Searches 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 | |||
| 2153 | When `diary-icalendar-summary-regexp' or | ||
| 2154 | `diary-icalendar-description-regexp' are non-nil, and the entry matches | ||
| 2155 | them, the matches will be used to generate the summary and description. | ||
| 2156 | |||
| 2157 | Otherwise, the first line of the entry (after any nonmarking symbol and | ||
| 2158 | date and time specification) is used as the summary. The description is | ||
| 2159 | the full body of the entry, excluding the nonmarking symbol, date and | ||
| 2160 | time, but including the summary. | ||
| 2161 | |||
| 2162 | Returns 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 | |||
| 2190 | Default 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 | ||
| 2193 | non-nil and the entry matches it, `icalendar-vtodo' is returned. | ||
| 2194 | |||
| 2195 | If the entry is nonmarking and `diary-icalendar-export-nonmarking-entries' | ||
| 2196 | is nil, nil is returned, indicating that the entry should not be | ||
| 2197 | exported." | ||
| 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 | |||
| 2213 | TYPE should be the type symbol for the component to be exported, as | ||
| 2214 | returned by `diary-icalendar-parse-entry-type'. If the entry is | ||
| 2215 | non-marking (i.e., begins with `diary-nonmarking-symbol'), and it is to | ||
| 2216 | be exported as an `icalendar-vevent' (according to TYPE), then this | ||
| 2217 | function returns a list containing the appropriate `icalendar-transp' | ||
| 2218 | property node to mark the event as transparent, and moves the current | ||
| 2219 | restriction 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 | |||
| 2233 | If a date is found, moves the current restriction past the end of the | ||
| 2234 | date and returns a list (MONTH DAY YEAR), where each value is an integer | ||
| 2235 | or 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 | |||
| 2301 | DATE-SEXP should be an S-expression in the variables `year', `month', | ||
| 2302 | `day', `monthname', and `dayname', as found e.g. in `diary-date-forms'. | ||
| 2303 | The returned regular expression matches dates of this form, including | ||
| 2304 | generic dates specified with \"*\", and abbreviated and long-form month | ||
| 2305 | and day names (based on `calendar-month-name-array' and | ||
| 2306 | `calendar-month-abbrev-array', and similarly for day names). The match | ||
| 2307 | groups contain the following data: | ||
| 2308 | |||
| 2309 | Group 1: the 2-4 digit year, or a literal * | ||
| 2310 | Group 2: the 1-2 digit month number, or a literal * | ||
| 2311 | Group 3: the 1-2 digit day number, or a literal * | ||
| 2312 | Group 4: the (long-form or abbreviated) month name, or a literal * | ||
| 2313 | Group 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 | |||
| 2343 | The day name must appear in `calendar-day-name-array' or | ||
| 2344 | `calendar-day-abbrev-array'. If a day name is found, move the current | ||
| 2345 | restriction past it, and return a day number between 0 (=Sunday) and | ||
| 2346 | 6 (=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 | |||
| 2368 | WEEKDAY must be an integer between 0 (=Sunday) and 6 (=Saturday). | ||
| 2369 | Returns a list (START RRULE), with START being an `icalendar-dtstart' | ||
| 2370 | property 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 | |||
| 2411 | Accepted 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 | |||
| 2416 | Group 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 | |||
| 2421 | Group 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 | |||
| 2429 | If a time specification is found, move the current restriction past it, | ||
| 2430 | and return a list (START END), where START and END are decoded-time | ||
| 2431 | values containing the hours and minutes slots parsed from the time | ||
| 2432 | specification. 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 | |||
| 2474 | The export strategy is determined by | ||
| 2475 | `diary-icalendar-time-zone-export-strategy', which see. | ||
| 2476 | |||
| 2477 | DT may be an `icalendar-date' or `icalendar-date-time'. If it is a | ||
| 2478 | date, it is returned unmodified. If it is a date-time, depending on the | ||
| 2479 | strategy and any existing zone information in DT, it will be converted | ||
| 2480 | to a correct local, UTC, or floating time. VTIMEZONE should be the | ||
| 2481 | `icalendar-vtimezone' which defines the local time zone, if the time | ||
| 2482 | zone 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 | |||
| 2509 | The S-expression must appear at the start of line, immediately after | ||
| 2510 | `diary-sexp-entry-symbol'. If an S-expression is found, move the | ||
| 2511 | current restriction past it, and return the S-expression. Otherwise, | ||
| 2512 | return 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'. | ||
| 2523 | Returns 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. | ||
| 2535 | Returns 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'. | ||
| 2546 | Returns a pair of nodes (START END). | ||
| 2547 | |||
| 2548 | VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in | ||
| 2549 | SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating | ||
| 2550 | times 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'. | ||
| 2561 | Returns 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'. | ||
| 2572 | Returns 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 | |||
| 2644 | SEXP must have the form (diary-offset INNER-SEXP NDAYS). The conversion | ||
| 2645 | is only possible for relatively simple cases of INNER-SEXP. The | ||
| 2646 | INNER-SEXP is first converted to a list of property nodes (see | ||
| 2647 | `diary-icalendar-export-sexp'), and then any date, time, period, and | ||
| 2648 | recurrence 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 | |||
| 2766 | COMPONENT should be an `icalendar-vevent' or `icalendar-vtodo'. The | ||
| 2767 | generated VALARM components will be added to this node's children. | ||
| 2768 | VTIMEZONE should define the local timezone; it is required when | ||
| 2769 | formatting 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 | |||
| 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. | ||
| 2841 | Returns 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 | |||
| 2845 | VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in | ||
| 2846 | SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating | ||
| 2847 | times 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 | |||
| 2886 | MONTHS, DAYS, and YEARS should either be integers, lists of integers, or | ||
| 2887 | the symbol t. | ||
| 2888 | |||
| 2889 | Returns a pair of nodes (START R), where START is an `icalendar-dtstart' | ||
| 2890 | node and R is an `icalendar-rrule' node or `icalendar-rdate' node (or | ||
| 2891 | nil, 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. | ||
| 2977 | Returns a pair of nodes (START R), where START is an `icalendar-dtstart' | ||
| 2978 | node 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 | |||
| 3019 | The enumeration starts on the current date and includes recurrences in | ||
| 3020 | the next `diary-icalendar-export-sexp-enumeration-days' days. Returns a | ||
| 3021 | list (START COMMENT RDATE), where START is an `icalendar-dtstart', | ||
| 3022 | COMMENT is an `icalendar-comment' containing SEXP, and RDATE is an | ||
| 3023 | `icalendar-rdate' containing the enumerated recurrences. If there are | ||
| 3024 | no recurrences, (START COMMENT EXDATE) is returned, where START is the | ||
| 3025 | current date, and EXDATE is an `icalendar-exdate' excluding that start | ||
| 3026 | date as a recurrence. (This is because `icalendar-dtstart' is a required | ||
| 3027 | property 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 | |||
| 3067 | The 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 | |||
| 3077 | There is partial support for `diary-offset' S-expressions; see | ||
| 3078 | `diary-icalendar-offset-to-nodes'. | ||
| 3079 | |||
| 3080 | Other S-expressions are only supported via enumeration. Their | ||
| 3081 | recurrences are enumerated for | ||
| 3082 | `diary-icalendar-export-sexp-enumeration-days' starting from the current | ||
| 3083 | date; see `diary-icalendar-other-sexp-to-recurrence'. If | ||
| 3084 | `diary-icalendar-export-sexp-enumerate-all' is non-nil, all | ||
| 3085 | S-expressions are enumerated rather than converted to recurrence rules. | ||
| 3086 | |||
| 3087 | VTIMEZONE, if specified, should be an `icalendar-vtimezone'. Times in | ||
| 3088 | SEXP will be reinterpreted as local to VTIMEZONE, as UTC, or as floating | ||
| 3089 | times 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 | |||
| 3110 | See `icalendar-recur-current-tz-to-vtimezone' for arguments' meanings. | ||
| 3111 | This function wraps that one, but signals `icalendar-diary-export-error' | ||
| 3112 | instead 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 | ||
| 3126 | distinct event; see `diary-icalendar-export-linewise'. | ||
| 3127 | Returns a list of component nodes representing the events. | ||
| 3128 | |||
| 3129 | VTIMEZONE must be the `icalendar-vtimezone' in which times in the entry | ||
| 3130 | appear (or nil). TYPE and DATE-NODES must contain the iCalendar component | ||
| 3131 | type and date information parsed from the beginning of the entry which | ||
| 3132 | apply to all of the events. These arguments are passed on in recursive | ||
| 3133 | calls 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 | |||
| 3159 | The region between BEGIN and END will be parsed for a date, time, | ||
| 3160 | summary, description, attendees, and UID. This information will be | ||
| 3161 | combined 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 | ||
| 3166 | wrapped in a list. Returns nil if the entry should not be exported | ||
| 3167 | according to `diary-icalendar-export-nonmarking-entries'. | ||
| 3168 | |||
| 3169 | If `diary-icalendar-export-linewise' is non-nil, then a top-level call | ||
| 3170 | to this function will return a list of several such components. (Thus, | ||
| 3171 | the function always returns a list of components.) | ||
| 3172 | |||
| 3173 | VTIMEZONE, if specified, should be the `icalendar-vtimezone' in which | ||
| 3174 | times 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 | |||
| 3178 | DATE-NODES and TYPE should be nil in a top-level call; they are used in | ||
| 3179 | recursive 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 | |||
| 3380 | If FILENAME exists and is not empty, this function asks whether to erase | ||
| 3381 | its contents first. If ERASE is non-nil, the contents of FILENAME will | ||
| 3382 | always be erased without asking. Otherwise the exported data will be | ||
| 3383 | appended to the end of FILENAME. | ||
| 3384 | |||
| 3385 | The export depends on a number of user-customizable variables. Before | ||
| 3386 | running this command for the first time, you may especially wish to | ||
| 3387 | check the values of: | ||
| 3388 | `diary-file' | ||
| 3389 | `diary-date-forms' | ||
| 3390 | `calendar-date-style' | ||
| 3391 | as 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 | |||
| 3458 | The diary entries in DIARY-FILENAME will be exported to iCalendar format | ||
| 3459 | and the resulting calendar will be saved to FILENAME. | ||
| 3460 | |||
| 3461 | If FILENAME exists and is not empty, this function asks whether to erase | ||
| 3462 | its contents first. If ERASE is non-nil, the contents of FILENAME will | ||
| 3463 | always be erased without asking. Otherwise the exported data will be | ||
| 3464 | appended to the end of FILENAME. | ||
| 3465 | |||
| 3466 | The export depends on a number of user-customizable variables. Before | ||
| 3467 | running this command for the first time, you may especially wish to | ||
| 3468 | check the values of: | ||
| 3469 | `diary-file' | ||
| 3470 | `diary-date-forms' | ||
| 3471 | `calendar-date-style' | ||
| 3472 | as 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 | |||
| 3502 | Entry applies if the queried date occurs between START and END, | ||
| 3503 | inclusive. 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 | |||
| 3513 | Entry applies if the queried date matches the recurrence rule. | ||
| 3514 | |||
| 3515 | The keyword arguments RULE, START, INCLUDE and EXCLUDE should contain | ||
| 3516 | the 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 | ||
| 3519 | and EXCLUDE should be lists of `icalendar-date' or `icalendar-date-time' | ||
| 3520 | values (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 | |||
| 3585 | This function allows you to display the data in an iCalendar-formatted | ||
| 3586 | file in the diary without importing it. The data is read directly from | ||
| 3587 | the currently value of `diary-file'. If this file contains iCalendar | ||
| 3588 | data, 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 | ||
| 3590 | the diary. (All three of these variables are dynamically bound by the | ||
| 3591 | diary when this function is called.) | ||
| 3592 | |||
| 3593 | To use this function, add an '#include \"FILE\"' entry in your diary | ||
| 3594 | file 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 | ||
| 3597 | the end of this hook if you want entries to be displayed in order.) | ||
| 3598 | Finally, add this function to `diary-nongregorian-listing-hook', so that | ||
| 3599 | it 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 | |||
| 3631 | INDEX should be a parse tree index containing the time zone definition | ||
| 3632 | relevant to COMPONENT; see `icalendar-parse-and-index'. The dates to | ||
| 3633 | mark are derived from COMPONENT's start and end date and time, and any | ||
| 3634 | recurrences it has within the year currently displayed by the calendar. | ||
| 3635 | |||
| 3636 | No dates are returned if COMPONENT's `icalendar-transp' property has the | ||
| 3637 | value \"TRANSPARENT\" (which means the component does not form a block | ||
| 3638 | of busy time on a schedule), or if COMPONENT is an `icalendar-vjournal' | ||
| 3639 | and `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 | |||
| 3681 | This function allows you to mark the dates in an iCalendar-formatted | ||
| 3682 | file in the calendar without importing it. The data is read directly | ||
| 3683 | from the current value of `diary-file' (which is dynamically bound by | ||
| 3684 | the diary when this function is called). | ||
| 3685 | |||
| 3686 | To use this function, add an '#include \"FILE\"' entry in your diary | ||
| 3687 | file 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 | ||
| 3691 | included 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. |
| 40 | See the documentation for the function `diary-include-other-diary-files'." | 40 | See 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. |
| 46 | Such days will then not be shown in the fancy diary buffer, even if they | 45 | Such days will then not be shown in the fancy diary buffer, even if they |
| 47 | are holidays." | 46 | are 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'. |
| 128 | See the documentation for the function `diary-list-sexp-entries'." | 122 | See 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 | |||
| 138 | can be only one comment on any line. | 131 | can be only one comment on any line. |
| 139 | See also `diary-comment-end'." | 132 | See 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. |
| 146 | The empty string means comments finish at the end of a line. | 138 | The empty string means comments finish at the end of a line. |
| 147 | See also `diary-comment-start'." | 139 | See 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. |
| 154 | Used for example by the appointment package - see `appt-activate'. | 145 | Used for example by the appointment package - see `appt-activate'. |
| 155 | The variables `number' and `original-date' are dynamically bound around | 146 | The variables `number' and `original-date' are dynamically bound around |
| 156 | the call." | 147 | the 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 | |||
| 201 | use the list-entries hook, whereas to process e.g. Islamic entries in | 190 | use the list-entries hook, whereas to process e.g. Islamic entries in |
| 202 | the main file and all included files, you would use the nongregorian hook." | 191 | the 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 |
| 219 | this hook is called." | 207 | this 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 | |||
| 264 | might include, for example, rearranging the lines into order by | 249 | might include, for example, rearranging the lines into order by |
| 265 | day and time, saving the buffer instead of deleting it, or | 250 | day and time, saving the buffer instead of deleting it, or |
| 266 | changing the function used to do the printing." | 251 | changing 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 | |||
| 273 | to be placed before those with times; 9999 would place entries | 257 | to be placed before those with times; 9999 would place entries |
| 274 | with no recognizable time after those with times." | 258 | with 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 | |||
| 302 | expressions that can involve the keywords `days' (a number), `date' | 283 | expressions 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 | |||
| 312 | is more than 50 years in the future, the previous century is assumed. | 292 | is more than 50 years in the future, the previous century is assumed. |
| 313 | If the result is more than 50 years in the past, the next century is assumed. | 293 | If the result is more than 50 years in the past, the next century is assumed. |
| 314 | If this variable is nil, years must be written in full." | 294 | If 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. |
| 403 | The format of the header is specified by `diary-header-line-format'." | 381 | The 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'. |
| 420 | Only used if `diary-header-line-flag' is non-nil." | 397 | Only 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." | |||
| 1671 | If you add this function to `diary-list-entries-hook', it should | 1646 | If you add this function to `diary-list-entries-hook', it should |
| 1672 | be the last item in the hook, in case earlier items add diary | 1647 | be the last item in the hook, in case earlier items add diary |
| 1673 | entries, or change the order." | 1648 | entries, 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." | |||
| 2120 | Prefix argument ARG makes the entry nonmarking." | 2095 | Prefix 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 | |||
| 139 | This function only checks that SYMBOL has been marked as a type; | ||
| 140 | it returns t for value types defined by `icalendar-define-type', | ||
| 141 | but also e.g. for types defined by `icalendar-define-param' and | ||
| 142 | `icalendar-define-property'. To check that SYMBOL names a value | ||
| 143 | type 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 | |||
| 152 | This means that SYMBOL must both satisfy `icalendar-type-symbol-p' and | ||
| 153 | have the property `icalendar-is-value'. It does not require the type to | ||
| 154 | be 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 | |||
| 162 | This is never t for value types or component types. For property and | ||
| 163 | parameter types defined with `icalendar-define-param' and | ||
| 164 | `icalendar-define-property', it is true if the :list-sep argument was | ||
| 165 | specified 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. | ||
| 197 | In component nodes, this is nil. Otherwise, it is a syntax node | ||
| 198 | representing 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 | |||
| 215 | This function is probably not what you want! It directly modifies the | ||
| 216 | type of NODE in-place, which could make the node invalid if its value or | ||
| 217 | children do not match the new TYPE. If you do not know in advance that | ||
| 218 | the data in NODE is compatible with the new TYPE, it is better to | ||
| 219 | construct 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 | |||
| 229 | This is a low-level constructor. If you are constructing iCalendar | ||
| 230 | syntax nodes directly in Lisp code, consider using one of the | ||
| 231 | higher-level macros based on `icalendar-make-node-from-templates' | ||
| 232 | instead, which expand to calls to this function but also perform type | ||
| 233 | checking and validation. | ||
| 234 | |||
| 235 | TYPE should be an iCalendar type symbol. CHILDREN, if given, should be | ||
| 236 | a list of syntax nodes. In property nodes, these should be the | ||
| 237 | parameters of the property. In component nodes, these should be the | ||
| 238 | properties or subcomponents of the component. CHILDREN should otherwise | ||
| 239 | be nil. | ||
| 240 | |||
| 241 | PROPS 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 | |||
| 304 | TYPES, if specified, should be a list of type symbols to check. | ||
| 305 | TYPES 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 | |||
| 328 | TYPE should be a symbol for an iCalendar value type, and VALUE should be | ||
| 329 | a value of that type. If TYPE is the symbol \\='plain-text, VALUE should | ||
| 330 | be a string, and in that case VALUE is returned as-is. | ||
| 331 | |||
| 332 | TYPE may also be a list of type symbols; in that case, the first type in | ||
| 333 | the list which VALUE satisfies is used as the returned node's type. If | ||
| 334 | the list is nil, VALUE will be checked against all types in | ||
| 335 | `icalendar-value-types'. | ||
| 336 | |||
| 337 | If VALUE is nil, and `icalendar-boolean' is not (in) TYPE, nil is | ||
| 338 | returned. Otherwise, a \\='wrong-type-argument error is signaled if | ||
| 339 | VALUE 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 | |||
| 401 | TYPE should be an iCalendar type symbol satisfying | ||
| 402 | `icalendar-param-type-symbol-p'; it should not be quoted. | ||
| 403 | |||
| 404 | VALUE should evaluate to a value appropriate for TYPE. In particular, if | ||
| 405 | TYPE expects a list of values (see `icalendar-expects-list-p'), VALUE | ||
| 406 | should be such a list. If necessary, the value(s) in VALUE will be | ||
| 407 | wrapped in syntax nodes indicating their type. | ||
| 408 | |||
| 409 | For example, | ||
| 410 | |||
| 411 | (icalendar-make-param icalendar-deltoparam | ||
| 412 | (list \"mailto:minionA@example.com\" \"mailto:minionB@example.com\")) | ||
| 413 | |||
| 414 | will return an `icalendar-deltoparam' node whose value is a list of | ||
| 415 | `icalendar-cal-address' nodes containing the two addresses. | ||
| 416 | |||
| 417 | The 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. | ||
| 430 | VALUE-TYPES should be a list of value types that TYPE accepts. | ||
| 431 | PARAMS, 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. | ||
| 447 | VALUE-TYPES should be a list of value types that TYPE accepts. | ||
| 448 | PARAMS, 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 | |||
| 463 | TYPE should be an iCalendar type symbol satisfying | ||
| 464 | `icalendar-property-type-symbol-p'; it should not be quoted. | ||
| 465 | |||
| 466 | VALUE should evaluate to a value appropriate for TYPE. In particular, | ||
| 467 | if TYPE expects a list of values (see | ||
| 468 | `icalendar-expects-list-of-values-p'), VALUE should be such a list. If | ||
| 469 | necessary, the value(s) in VALUE will be wrapped in syntax nodes | ||
| 470 | indicating their type. If VALUE is not of the default value type for | ||
| 471 | TYPE, an `icalendar-valuetypeparam' will automatically be added to | ||
| 472 | PARAM-TEMPLATES. | ||
| 473 | |||
| 474 | Each element of PARAM-TEMPLATES should represent a parameter node; see | ||
| 475 | `icalendar-make-node-from-templates' for the format of such templates. | ||
| 476 | A template can also have the form (@ L), where L evaluates to a list of | ||
| 477 | parameter nodes to be added to the component. | ||
| 478 | |||
| 479 | PARAM-TEMPLATES which evaluate to nil are removed when the property node | ||
| 480 | is constructed. | ||
| 481 | |||
| 482 | For example, | ||
| 483 | |||
| 484 | (icalendar-make-property icalendar-rdate (list \\='(2 1 2025) \\='(3 1 2025))) | ||
| 485 | |||
| 486 | will return an `icalendar-rdate' node whose value is a list of | ||
| 487 | `icalendar-date' nodes containing the dates above as their values. | ||
| 488 | |||
| 489 | The 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 | |||
| 526 | TYPE should be an iCalendar type symbol satisfying | ||
| 527 | `icalendar-component-type-symbol-p'; it should not be quoted. | ||
| 528 | |||
| 529 | Each expression in TEMPLATES should represent a child node of the | ||
| 530 | component; see `icalendar-make-node-from-templates' for the format of | ||
| 531 | such TEMPLATES. A template can also have the form (@ L), where L | ||
| 532 | evaluates to a list of child nodes to be added to the component. | ||
| 533 | |||
| 534 | Any value in TEMPLATES that evaluates to nil will be removed before the | ||
| 535 | component node is constructed. | ||
| 536 | |||
| 537 | If TYPE is `icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', | ||
| 538 | or `icalendar-vfreebusy', the properties `icalendar-dtstamp' and | ||
| 539 | `icalendar-uid' will be automatically provided, if they are absent in | ||
| 540 | TEMPLATES. Likewise, if TYPE is `icalendar-vcalendar', the properties | ||
| 541 | `icalendar-prodid', `icalendar-version', and `icalendar-calscale' will | ||
| 542 | be automatically provided if absent. | ||
| 543 | |||
| 544 | For example, | ||
| 545 | |||
| 546 | (icalendar-make-component icalendar-vevent | ||
| 547 | (icalendar-summary \"Party\") | ||
| 548 | (icalendar-location \"Robot House\") | ||
| 549 | (@ list-of-other-properties)) | ||
| 550 | |||
| 551 | will return an `icalendar-vevent' node containing the provided | ||
| 552 | properties as well as `icalendar-dtstamp' and `icalendar-uid' | ||
| 553 | properties. | ||
| 554 | |||
| 555 | The 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 | |||
| 613 | TYPE should be an iCalendar type symbol; it should not be quoted. This | ||
| 614 | macro (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 | ||
| 619 | to write iCalendar syntax nodes of TYPE as Lisp code. | ||
| 620 | |||
| 621 | Each expression in TEMPLATES represents a child node of the constructed | ||
| 622 | node. It must either evaluate to such a node, or it must have one of | ||
| 623 | the 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 | |||
| 642 | If TYPE is an iCalendar component or property type, a TEMPLATE can also | ||
| 643 | have the form (@ L), where L evaluates to a list of child nodes to be | ||
| 644 | added to the component or property node. | ||
| 645 | |||
| 646 | For 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 | |||
| 663 | Before 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. | ||
| 682 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 683 | See `icalendar-vcalendar' for the permissible child types. | ||
| 684 | |||
| 685 | If TEMPLATES does not contain templates for the `icalendar-prodid' and | ||
| 686 | `icalendar-version' properties, they will be automatically added; see | ||
| 687 | the 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. | ||
| 693 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 694 | See `icalendar-vevent' for the permissible child types. | ||
| 695 | |||
| 696 | If TEMPLATES does not contain templates for the `icalendar-dtstamp' and | ||
| 697 | `icalendar-uid' properties (both required), they will be automatically | ||
| 698 | provided." | ||
| 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. | ||
| 703 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 704 | See `icalendar-vtodo' for the permissible child types. | ||
| 705 | |||
| 706 | If TEMPLATES does not contain templates for the `icalendar-dtstamp' and | ||
| 707 | `icalendar-uid' properties (both required), they will be automatically | ||
| 708 | provided." | ||
| 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. | ||
| 713 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 714 | See `icalendar-vjournal' for the permissible child types. | ||
| 715 | |||
| 716 | If TEMPLATES does not contain templates for the `icalendar-dtstamp' and | ||
| 717 | `icalendar-uid' properties (both required), they will be automatically | ||
| 718 | provided." | ||
| 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. | ||
| 723 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 724 | See `icalendar-vfreebusy' for the permissible child types. | ||
| 725 | |||
| 726 | If TEMPLATES does not contain templates for the `icalendar-dtstamp' and | ||
| 727 | `icalendar-uid' properties (both required), they will be automatically | ||
| 728 | provided." | ||
| 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. | ||
| 733 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 734 | See `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. | ||
| 739 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 740 | See `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. | ||
| 745 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 746 | See `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. | ||
| 751 | See `icalendar-make-node-from-templates' for the format of TEMPLATES. | ||
| 752 | See `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. | ||
| 781 | Signals an `icalendar-validation-error' if NODE's value is | ||
| 782 | invalid, 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. | ||
| 853 | Returns an alist mapping type symbols to the number of NODE's children | ||
| 854 | of 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 | |||
| 865 | The :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. | ||
| 868 | Signals an `icalendar-validation-error' if NODE is invalid, or returns | ||
| 869 | NODE. | ||
| 870 | |||
| 871 | Note that this function does not check that the children of NODE | ||
| 872 | are 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. | ||
| 919 | By default, the check will only validate NODE itself, but if | ||
| 920 | RECURSIVELY is non-nil, it will recursively check all its | ||
| 921 | descendants as well. Signals an `icalendar-validation-error' if | ||
| 922 | NODE 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 | |||
| 80 | PRINT-NAME should be the string used to represent this type in | ||
| 81 | the value of an `icalendar-valuetypeparam' property parameter, or | ||
| 82 | nil if this is not a type that should be specified there. DOC | ||
| 83 | should be a documentation string for the type. SPECIFIER should | ||
| 84 | be a type specifier in the sense of `cl-deftype'. MATCHER should | ||
| 85 | be an RX definition body (see `rx-define'; argument lists are not | ||
| 86 | supported). | ||
| 87 | |||
| 88 | Before the type is defined with `cl-deftype', a function will be | ||
| 89 | defined named `icalendar-match-PRINT-NAME-value' | ||
| 90 | \(or `icalendar-match-OTHER-value', if PRINT-NAME is nil, where | ||
| 91 | OTHER is derived from SYMBOLIC-NAME by removing any prefix | ||
| 92 | \"icalendar-\" and suffix \"value\"). This function takes a | ||
| 93 | string argument and matches it against MATCHER. This function may | ||
| 94 | thus occur in SPECIFIER (e.g. in a (satisfies ...) clause). | ||
| 95 | |||
| 96 | See the functions `icalendar-read-value-node', | ||
| 97 | `icalendar-parse-value-node', and `icalendar-print-value-node' to | ||
| 98 | convert values defined with this macro to and from their text | ||
| 99 | representation in iCalendar format. | ||
| 100 | |||
| 101 | The 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', | ||
| 131 | and 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. | ||
| 173 | String values matching MATCHER are assumed to be type-specific keywords | ||
| 174 | that should be interned as symbols when read. (Thus no type specifier | ||
| 175 | is necessary: it is always just \\='symbol.) Their printed | ||
| 176 | representation 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. | ||
| 200 | PARAM-NAME should be the parameter name as it should appear in | ||
| 201 | iCalendar data. | ||
| 202 | |||
| 203 | VALUE should either be a symbol for a value type defined with | ||
| 204 | `icalendar-define-type', or an `rx' regular expression. If it is | ||
| 205 | a type symbol, the regex, reader and printer functions associated | ||
| 206 | with that type will be used when parsing and serializing values. | ||
| 207 | If it is a regular expression, it is assumed that the values of | ||
| 208 | this parameter are strings which match that regular expression. | ||
| 209 | |||
| 210 | An `rx' regular expression named SYMBOLIC-NAME which matches the | ||
| 211 | parameter 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 | |||
| 219 | This regex matches the entire string representing this parameter, | ||
| 220 | from \";\" to the end of its value. Another regular expression | ||
| 221 | named `SYMBOLIC-NAME-value' is also defined to match just the | ||
| 222 | value part, after \";PARAM-NAME=\", with groups 2 and 3 as above. | ||
| 223 | |||
| 224 | A 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, | ||
| 227 | where OTHER is derived from SYMBOLIC-NAME by removing any prefix | ||
| 228 | `icalendar-' and suffix `param'). This function is used | ||
| 229 | to provide syntax highlighting in `icalendar-mode'. | ||
| 230 | |||
| 231 | See 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 | ||
| 234 | this macro to and from their text representation in iCalendar | ||
| 235 | format. | ||
| 236 | |||
| 237 | The 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. | ||
| 411 | PROPERTY-NAME should be the property name as it should appear in | ||
| 412 | iCalendar data. | ||
| 413 | |||
| 414 | VALUE should either be a symbol for a value type defined with | ||
| 415 | `icalendar-define-type', or an `rx' regular expression. If it is | ||
| 416 | a type symbol, the regex, reader and printer functions associated | ||
| 417 | with that type will be used when parsing and serializing the | ||
| 418 | property's value. If it is a regular expression, it is assumed | ||
| 419 | that the values are strings of type `icalendar-text' which match | ||
| 420 | that regular expression. | ||
| 421 | |||
| 422 | An `rx' regular expression named SYMBOLIC-NAME is defined to | ||
| 423 | match 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 | |||
| 431 | This regex matches the entire string representing this property, | ||
| 432 | from the beginning of the content line to the end of its value. | ||
| 433 | Another regular expression named `SYMBOLIC-NAME-value' is also | ||
| 434 | defined to match just the value part, after the separating colon, | ||
| 435 | with groups 2 and 3 as above. | ||
| 436 | |||
| 437 | A function to match the complete property expression called | ||
| 438 | `icalendar-match-PROPERTY-NAME-property' is defined. This | ||
| 439 | function is used to provide syntax highlighting in | ||
| 440 | `icalendar-mode'. | ||
| 441 | |||
| 442 | See the functions `icalendar-read-property-value', | ||
| 443 | `icalendar-parse-property-value', `icalendar-parse-property', and | ||
| 444 | `icalendar-print-property-node' to convert properties defined | ||
| 445 | with this macro to and from their text representation in | ||
| 446 | iCalendar format. | ||
| 447 | |||
| 448 | The 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 | ||
| 486 | of 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. | ||
| 654 | COMPONENT-NAME should be the name of the component as it should | ||
| 655 | appear in iCalendar data. | ||
| 656 | |||
| 657 | Regular expressions to match the component boundaries are defined | ||
| 658 | named `COMPONENT-NAME-begin' and `COMPONENT-NAME-end' (or | ||
| 659 | `OTHER-begin' and `OTHER-end', where `OTHER' is derived from | ||
| 660 | SYMBOLIC-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 | |||
| 666 | A 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 | ||
| 669 | function is used to provide syntax highlighting in | ||
| 670 | `icalendar-mode'. | ||
| 671 | |||
| 672 | The following keyword arguments are accepted: | ||
| 673 | |||
| 674 | :child-spec - a plist mapping the following keywords to lists | ||
| 675 | of 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. | ||
| 805 | NODE should be an iCalendar syntax node representing a component or | ||
| 806 | property. | ||
| 807 | |||
| 808 | Each 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 | |||
| 950 | NODE should be an iCalendar syntax node representing an iCalendar | ||
| 951 | component: `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 | |||
| 955 | Each 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 | |||
| 982 | NODE should be an iCalendar syntax node representing a property or | ||
| 983 | parameter. If NODE is not a syntax node, this form evalutes to nil | ||
| 984 | without binding the variables in BINDINGS and without executing BODY. | ||
| 985 | |||
| 986 | Within 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 | ||
| 989 | bound to `value-node's value. | ||
| 990 | |||
| 991 | If 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 | ||
| 993 | bound to a list of their types, and `values' will be bound to their | ||
| 994 | values. | ||
| 995 | |||
| 996 | If NODE's value is not a syntax node, then `value' is instead bound | ||
| 997 | directly to NODE's value, and `value-type' and `value-node' are bound to | ||
| 998 | nil. | ||
| 999 | |||
| 1000 | If BODY is nil, it is assumed to be the symbol `value'; thus | ||
| 1001 | (icalendar-with-node-value some-node) | ||
| 1002 | is equivalent to | ||
| 1003 | (icalendar-with-node-value some-node nil value) | ||
| 1004 | |||
| 1005 | BINDINGS are passed on to `icalendar-with-node-children' and will be | ||
| 1006 | available 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 | |||
| 1037 | NODE should be an iCalendar syntax node representing a property. If NODE | ||
| 1038 | is not a syntax node, this form evalutes to nil without binding the | ||
| 1039 | variables in BINDINGS and without executing BODY. | ||
| 1040 | |||
| 1041 | Within 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 | ||
| 1044 | bound to `value-node's value. | ||
| 1045 | |||
| 1046 | If 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 | ||
| 1048 | bound to a list of their types, and `values' will be bound to their | ||
| 1049 | values. | ||
| 1050 | |||
| 1051 | If NODE's value is not a syntax node, then `value' is bound directly to | ||
| 1052 | NODE's value, and `value-type' and `value-node' are bound to nil. | ||
| 1053 | |||
| 1054 | BINDINGS are passed on to `icalendar-with-node-children' and will be | ||
| 1055 | available 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 | |||
| 1060 | PARAMETER should be an iCalendar syntax node representing a | ||
| 1061 | parameter. If PARAMETER is nil, this form evalutes to nil without | ||
| 1062 | executing BODY. | ||
| 1063 | |||
| 1064 | Within 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 | ||
| 1066 | bound to the value node's type, and `value' will be bound to the value | ||
| 1067 | node's value. | ||
| 1068 | |||
| 1069 | If PARAMETER's value is not a syntax node, then `value' is bound | ||
| 1070 | directly to PARAMETER's value, and `value-type' and `value-node' are | ||
| 1071 | bound 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 | |||
| 1079 | Find the first child node of type TYPE in NODE, bind that | ||
| 1080 | child node's value and any of its children in BINDINGS and execute BODY | ||
| 1081 | with these bindings. If there is no such node, this form evalutes to | ||
| 1082 | nil without executing BODY. | ||
| 1083 | |||
| 1084 | Within BODY, the symbols `value-node', `value-type', and `value' will be | ||
| 1085 | bound as in `icalendar-with-node-value'. | ||
| 1086 | If BODY is nil, it is assumed to be the symbol `value'; thus | ||
| 1087 | (icalendar-with-child-of some-node some-type) | ||
| 1088 | is equivalent to | ||
| 1089 | (icalendar-with-child-of some-node some-type nil value) | ||
| 1090 | |||
| 1091 | See `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 | |||
| 1101 | Find the first property node of type TYPE in NODE and execute BODY. | ||
| 1102 | |||
| 1103 | Within BODY, the symbols `value-node', `value-type', and `value' will be | ||
| 1104 | bound 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 | ||
| 1106 | symbol `value'; thus | ||
| 1107 | (icalendar-with-property-of some-component some-type) | ||
| 1108 | is equivalent to | ||
| 1109 | (icalendar-with-property-of some-component some-type nil value) | ||
| 1110 | |||
| 1111 | BINDINGS 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 | |||
| 1117 | Find the first parameter node of TYPE in NODE and execute BODY. | ||
| 1118 | |||
| 1119 | Within BODY, the symbols `value-node', `value-type', and `value' will be | ||
| 1120 | bound 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 | ||
| 1122 | symbol `value'; thus | ||
| 1123 | (icalendar-with-param-of some-property some-type) | ||
| 1124 | is 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. | ||
| 94 | These 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. | ||
| 503 | This 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. | ||
| 510 | Auto-fill-mode interferes with line folding and syntax highlighting, so | ||
| 511 | it is off by default in iCalendar buffers. This function is intended to | ||
| 512 | be 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. | ||
| 522 | The new buffer will contain the same data as the current buffer, but | ||
| 523 | with content lines unfolded (before decoding, if possible). | ||
| 524 | |||
| 525 | `Folding' means inserting a line break and a single whitespace | ||
| 526 | character to continue lines longer than 75 octets; `unfolding' | ||
| 527 | means removing the extra whitespace inserted by folding. The | ||
| 528 | iCalendar standard (RFC5545) requires folding lines when | ||
| 529 | serializing data to iCalendar format, and unfolding before | ||
| 530 | parsing it. In `icalendar-mode', folded lines may not have proper | ||
| 531 | syntax highlighting; this command allows you to view iCalendar | ||
| 532 | data with proper syntax highlighting, as the parser sees it. | ||
| 533 | |||
| 534 | If the current buffer is visiting a file, this function will | ||
| 535 | offer to save the buffer first, and then reload the contents from | ||
| 536 | the file, performing unfolding with `icalendar-unfold-undecoded-region' | ||
| 537 | before decoding it. This is the most reliable way to unfold lines. | ||
| 538 | |||
| 539 | If it is not visiting a file, it will unfold the new buffer | ||
| 540 | with `icalendar-unfold-region'. This can in some cases have | ||
| 541 | undesirable effects (see its docstring), so the original contents | ||
| 542 | are preserved unchanged in the current buffer. | ||
| 543 | |||
| 544 | In both cases, after switching to the new buffer, this command | ||
| 545 | offers to kill the original buffer. | ||
| 546 | |||
| 547 | It is recommended to turn off `auto-fill-mode' when viewing an | ||
| 548 | unfolded buffer, so that filling does not interfere with syntax | ||
| 549 | highlighting. This function offers to disable `auto-fill-mode' if | ||
| 550 | it 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 | |||
| 577 | This mode provides syntax highlighting for iCalendar components, | ||
| 578 | properties, values, and property parameters, and defines a format to | ||
| 579 | automatically handle folding and unfolding iCalendar content lines. | ||
| 580 | |||
| 581 | `Folding' means inserting whitespace characters to continue long | ||
| 582 | lines; `unfolding' means removing the extra whitespace inserted | ||
| 583 | by folding. The iCalendar standard requires folding lines when | ||
| 584 | serializing data to iCalendar format, and unfolding before | ||
| 585 | parsing it. | ||
| 586 | |||
| 587 | Thus icalendar-mode's syntax highlighting is designed to work with | ||
| 588 | unfolded lines. When `icalendar-mode' is activated in a buffer, it will | ||
| 589 | automatically unfold lines using a file format conversion, and | ||
| 590 | automatically fold lines when saving the buffer to a file; see Info | ||
| 591 | node `(elisp)Format Conversion' for more information. It also disables | ||
| 592 | `auto-fill-mode' if it is active, since filling interferes with line | ||
| 593 | folding 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 | |||
| 91 | By default, the iCalendar parser accepts certain harmless deviations | ||
| 92 | from RFC5545 that are common in real-world data (e.g., unescaped commas | ||
| 93 | in text values). Setting this to t will cause the parser to produce | ||
| 94 | errors 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 | |||
| 118 | The functions in this hook will be run before the iCalendar data is | ||
| 119 | \"unfolded\", i.e., before whitespace introduced for breaking long lines | ||
| 120 | is removed (see `icalendar-unfold-region' and | ||
| 121 | `icalendar-unfold-undecoded-region'). If you routinely receive | ||
| 122 | iCalendar data that is not correctly folded, you can add functions to | ||
| 123 | this hook which clean up that data before unfolding is attempted. | ||
| 124 | |||
| 125 | Each function should accept zero arguments and should perform its | ||
| 126 | operation 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. | ||
| 133 | This 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. | ||
| 145 | If omitted, BUFFER defaults to the current buffer. | ||
| 146 | |||
| 147 | \"Unfolding\" means removing the whitespace characters inserted to | ||
| 148 | continue lines longer than 75 octets (see `icalendar-fold-region' | ||
| 149 | for the folding operation). RFC5545 specifies these whitespace | ||
| 150 | characters to be a CR-LF sequence followed by a single space or | ||
| 151 | tab character. Unfolding can only be done reliably before a | ||
| 152 | region is decoded, since decoding potentially replaces CR-LF line | ||
| 153 | endings. | ||
| 154 | |||
| 155 | When `icalendar-parse-strictly' is non-nil, this function searches | ||
| 156 | strictly for CR-LF sequences and will fail if they have already been | ||
| 157 | replaced, so it should only be called with a region that has not yet | ||
| 158 | been decoded. Otherwise, it also searches for folds containing | ||
| 159 | Unix-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 | ||
| 181 | continue lines longer than 75 octets (see `icalendar-fold-region' | ||
| 182 | for the folding operation). | ||
| 183 | |||
| 184 | Returns the new end position after unfolding finishes. Thus this | ||
| 185 | function is a suitable FROM-FN (decoding function) for `format-alist'. | ||
| 186 | |||
| 187 | WARNING: Unfolding can only be done reliably before text is | ||
| 188 | decoded, since decoding potentially replaces CR-LF line endings. | ||
| 189 | Unfolding an already-decoded region could lead to unexpected | ||
| 190 | results, such as displaying multibyte characters incorrectly, | ||
| 191 | depending on the contents and the coding system used. | ||
| 192 | |||
| 193 | This function attempts to do the right thing even if the region | ||
| 194 | is already decoded. If it is still undecoded, it is better to | ||
| 195 | call `icalendar-unfold-undecoded-region' directly instead, and | ||
| 196 | decode 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 | |||
| 228 | Copies the buffer contents between START and END (in BUFFER, if | ||
| 229 | provided) to a new buffer and performs line unfolding in the new buffer | ||
| 230 | with `icalendar-unfold-region'. That function can in some cases have | ||
| 231 | undesirable effects; see its docstring. If BUFFER is visiting a file, it | ||
| 232 | may be better to reload its contents from that file and perform line | ||
| 233 | unfolding before decoding; see `icalendar-unfolded-buffer-from-file'. | ||
| 234 | Returns 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 | |||
| 261 | Copies the contents of BUFFER to a new buffer and performs line | ||
| 262 | unfolding there with `icalendar-unfold-region'. That function can in | ||
| 263 | some cases have undesirable effects; see its docstring. If BUFFER is | ||
| 264 | visiting a file, it may be better to reload its contents from that file | ||
| 265 | and 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 | |||
| 287 | If an unfolded buffer is already visiting FILENAME, return | ||
| 288 | it. Otherwise, create a new buffer with the contents of FILENAME and | ||
| 289 | perform line unfolding with `icalendar-unfold-undecoded-region', then | ||
| 290 | decode the buffer, setting an appropriate value for | ||
| 291 | `buffer-file-coding-system', and return the new buffer. Optional | ||
| 292 | arguments 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 | ||
| 333 | character at the beginning of the new line. If USE-TABS is | ||
| 334 | non-nil, insert a tab character instead of a single space. | ||
| 335 | |||
| 336 | RFC5545 specifies that lines longer than 75 *octets* (excluding | ||
| 337 | the line-ending CR-LF sequence) must be folded, and allows that | ||
| 338 | some implementations might fold lines in the middle of a | ||
| 339 | multibyte character. This function takes care not to do that in a | ||
| 340 | buffer where `enable-multibyte-characters' is non-nil, and only | ||
| 341 | folds between character boundaries. If the buffer is in unibyte | ||
| 342 | mode, however, and contains undecoded multibyte data, it may fold | ||
| 343 | lines in the middle of a multibyte character. | ||
| 344 | |||
| 345 | By default, this function modifies the region by inserting line folds. | ||
| 346 | If the optional argument ANNOTATE-ONLY is non-nil, it will instead leave | ||
| 347 | the buffer unmodified, and return a list of \"annotations\" | ||
| 348 | \(POSITION . LINE-FOLD), indicating where line folds in the region should | ||
| 349 | be inserted. This output is suitable for a function in | ||
| 350 | `write-region-annotation-functions'; `icalendar-folding-annotations' | ||
| 351 | is 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 | |||
| 385 | This function is a wrapper for `icalendar-fold-region' that provides the | ||
| 386 | interface to be used from `write-region-annotation-functions', which | ||
| 387 | see." | ||
| 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 | |||
| 403 | BUFFER defaults to the current buffer. Folded content lines need to be | ||
| 404 | unfolded before parsing the buffer or performing syntax | ||
| 405 | highlighting. 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. | ||
| 414 | BUFFER 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 | |||
| 420 | Lines longer than 75 bytes need to folded before saving or transmitting | ||
| 421 | the data in BUFFER (default: current buffer). If BUFFER contains such | ||
| 422 | lines, return the position at the beginning of the first line that | ||
| 423 | requires 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. | ||
| 444 | BUFFER 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. | ||
| 515 | S 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 | |||
| 537 | TYPES should be a list of type symbols. For each type in TYPES, the | ||
| 538 | parser function associated with that type will be called at point. The | ||
| 539 | return value of the first successful parser function is returned. If | ||
| 540 | none 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 | |||
| 566 | READER should be a reader function that accepts a single string argument. | ||
| 567 | SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string'. | ||
| 568 | SEPARATORS defaults to \"[^\\][,;]\". TRIM defaults to matching a | ||
| 569 | double quote character. | ||
| 570 | |||
| 571 | VALUE-REGEX should be a regular expression if READER assumes that | ||
| 572 | individual substrings in STRING have previously been matched | ||
| 573 | against this regex. In this case, each value in S is placed in a | ||
| 574 | temporary buffer and the match against VALUE-REGEX is performed | ||
| 575 | before 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 | |||
| 603 | TYPE should be a value type symbol. The reader function | ||
| 604 | associated with that type will be called to read the successive | ||
| 605 | values in STRING, and the values will be returned as a list of | ||
| 606 | syntax nodes. | ||
| 607 | |||
| 608 | SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string' and | ||
| 609 | will 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. | ||
| 617 | TYPE 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 | |||
| 623 | If VAL is a string, just return it unchanged. | ||
| 624 | |||
| 625 | Otherwise, VAL should be a syntax node representing a value. In | ||
| 626 | that case, return the original string value if another was | ||
| 627 | substituted at parse time, or look up the printer function for | ||
| 628 | the node's type and call it on the value inside the node. | ||
| 629 | |||
| 630 | For properties and parameters that only allow a single value, | ||
| 631 | this function should be a sufficient value printer. It is not | ||
| 632 | sufficient for those that allow lists of values, or which have | ||
| 633 | other 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. | ||
| 742 | Value type strings are those which can appear in `icalendar-valuetypeparam' | ||
| 743 | parameters 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. | ||
| 747 | Returns 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. | ||
| 753 | Returns 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 | |||
| 778 | This means that SYMBOL names a type for a property or parameter value | ||
| 779 | defined by `icalendar-define-type' which has a print name (mainly for | ||
| 780 | use in `icalendar-valuetypeparam' parameters). That is, SYMBOL must *both* | ||
| 781 | satisfy `icalendar-value-type-symbol-p' and be associated with a print | ||
| 782 | name 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 | |||
| 799 | The parsed and printed representations are the same: a string of characters | ||
| 800 | representing 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. | ||
| 810 | S 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 | |||
| 824 | When printed, either the string 'TRUE' or 'FALSE'. | ||
| 825 | When 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. | ||
| 853 | S 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 | |||
| 869 | When printed, a date is a string of digits in YYYYMMDD format. | ||
| 870 | |||
| 871 | When read, a date is a list (MONTH DAY YEAR), with the three | ||
| 872 | values being integers in the appropriate ranges; see calendar.el | ||
| 873 | for 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. | ||
| 888 | S 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*. | ||
| 915 | This predicate does not check date-related values in VAL; | ||
| 916 | for 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 | |||
| 928 | When printed, a time is a string of six digits HHMMSS, followed | ||
| 929 | by the letter 'Z' if it is in UTC. | ||
| 930 | |||
| 931 | When 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 | ||
| 934 | read, the DAY, MONTH, YEAR, and DOW fields are nil, and these | ||
| 935 | fields 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. | ||
| 971 | S 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 | |||
| 1013 | When printed, a date-time is a string of digits like: | ||
| 1014 | YYYYMMDDTHHMMSS | ||
| 1015 | where the 'T' is literal, and separates the date string from the | ||
| 1016 | time string. | ||
| 1017 | |||
| 1018 | When 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. | ||
| 1051 | S 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 | |||
| 1119 | When 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 | |||
| 1127 | For example, a duration of 15 days, 5 hours, and 20 seconds would be printed: | ||
| 1128 | P15DT5H0M20S | ||
| 1129 | and a duration of 7 weeks would be printed: | ||
| 1130 | P7W | ||
| 1131 | |||
| 1132 | When read, a duration is either an integer, in which case it | ||
| 1133 | represents a number of weeks, or a decoded time, in which case it | ||
| 1134 | must represent a time delta in the sense of `decoded-time-add'. | ||
| 1135 | Note that, in the time delta representation, units of time longer | ||
| 1136 | than a day are not supported and will be ignored if present. | ||
| 1137 | |||
| 1138 | This type is named `icalendar-dur-value' rather than | ||
| 1139 | `icalendar-duration' for consistency with the text of RFC5545 and | ||
| 1140 | so 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 | |||
| 1162 | When printed, possibly a sign + or -, followed by a sequence of digits, | ||
| 1163 | and 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 | |||
| 1177 | When printed, possibly a sign + or -, followed by a sequence of digits. | ||
| 1178 | When 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. | ||
| 1202 | If the end is not explicitly specified, it will be computed from the | ||
| 1203 | period'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 | |||
| 1225 | START and END (if given) should be `icalendar-date-time' values. | ||
| 1226 | DURATION, if given, should be an `icalendar-dur-value'. It is an error | ||
| 1227 | to 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. | ||
| 1236 | S 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 | |||
| 1258 | A period of time is specified as a starting date-time together | ||
| 1259 | with either an explicit date-time as its end, or a duration which | ||
| 1260 | gives its length and implicitly marks its end. | ||
| 1261 | |||
| 1262 | When printed, the starting date-time is separated from the end or | ||
| 1263 | duration by a / character. | ||
| 1264 | |||
| 1265 | When read, a period is represented as a list (START END DUR), where | ||
| 1266 | START is an `icalendar-date-time', END is either an | ||
| 1267 | `icalendar-date-time' or nil, and DUR is either an `icalendar-dur-value' | ||
| 1268 | or 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. | ||
| 1323 | Weekday abbreviations in recurrence rule parts are translated to | ||
| 1324 | and from numbers for compatibility with calendar-* and | ||
| 1325 | decoded-time-* functions.") | ||
| 1326 | |||
| 1327 | (defun ical:read-weekdaynum (s) | ||
| 1328 | "Read a weekday abbreviation to a number. | ||
| 1329 | If the abbreviation is preceded by an offset, read a dotted | ||
| 1330 | pair (WEEKDAY . OFFSET). Thus \"SU\" becomes 0, \"-1SU\" | ||
| 1331 | becomes (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. | ||
| 1343 | The result is in the format required for a BYDAY recurrence rule clause. | ||
| 1344 | See `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. | ||
| 1355 | S should have been matched against `icalendar-recur-rule-part'. | ||
| 1356 | The return value is a list (KEYWORD VALUE), where VALUE may | ||
| 1357 | itself 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. | ||
| 1426 | S 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). | ||
| 1466 | DAYNO 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 | |||
| 1530 | When printed, a recurrence rule value looks like | ||
| 1531 | KEY1=VAL1;KEY2=VAL2;... | ||
| 1532 | where the VALs may themselves be lists or have other syntactic | ||
| 1533 | structure; see RFC5545 sec. 3.3.10 for all the details. | ||
| 1534 | |||
| 1535 | The KEYs and their associated value types when read are as follows. | ||
| 1536 | The first is required: | ||
| 1537 | '(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY) | ||
| 1538 | These two are mutually exclusive; at most one may appear: | ||
| 1539 | UNTIL (or icalendar-date-time icalendar-date) | ||
| 1540 | COUNT (integer 1 *) | ||
| 1541 | All 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 | |||
| 1556 | When read, these KEYs and their associated VALs are gathered into | ||
| 1557 | an alist. | ||
| 1558 | |||
| 1559 | In general, the VALs consist of integers or lists of integers. | ||
| 1560 | Abbreviations for weekday names are translated into integers | ||
| 1561 | 0 (=Sunday) through 6 (=Saturday), for compatibility with | ||
| 1562 | calendar.el and decoded-time-* functions. | ||
| 1563 | |||
| 1564 | Some examples: | ||
| 1565 | |||
| 1566 | 1) 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 | |||
| 1572 | 2) 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 | |||
| 1579 | 3) 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 | |||
| 1585 | Notice that singleton values are still wrapped in a list when the | ||
| 1586 | KEY accepts a list of values, but not when the KEY always has a | ||
| 1587 | single (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. | ||
| 1612 | If no starting weekday is specified in RECUR-VALUE, returns the default, | ||
| 1613 | 1 (= 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. | ||
| 1618 | BYUNIT should be a symbol: \\='BYMONTH, \\='BYDAY, etc. | ||
| 1619 | See `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. | ||
| 1643 | The region is taken from BUFFER between BEGIN and END. BUFFER defaults | ||
| 1644 | to the current buffer, and BEGIN and END default to point and mark in | ||
| 1645 | BUFFER." | ||
| 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. | ||
| 1664 | Unescaping replaces literal '\\n' and '\\N' with newline, and removes | ||
| 1665 | backslashes 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. | ||
| 1676 | Unescaping replaces literal '\\n' and '\\N' with newline, and removes | ||
| 1677 | backslashes 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. | ||
| 1685 | Escaping replaces newlines with literal '\\n', and escapes commas, | ||
| 1686 | semicolons 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. | ||
| 1698 | Escaping replaces newlines with literal '\\n', and escapes commas, | ||
| 1699 | semicolons 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. | ||
| 1707 | S 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. | ||
| 1712 | VAL may be a string or text region (see `icalendar-make-text-region'). | ||
| 1713 | The text will be escaped before printing. If VAL is a region, the text | ||
| 1714 | it 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. | ||
| 1728 | The 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 | |||
| 1744 | Text values can be represented in Elisp in two ways: as strings, | ||
| 1745 | or as buffer regions. For values which aren't expected to change, | ||
| 1746 | such as property values in a text/calendar email attachment, use | ||
| 1747 | strings. For values which are user-editable and might change | ||
| 1748 | between parsing and serializing to iCalendar format, use a | ||
| 1749 | region. In that case, a text value contains two markers BEGIN and | ||
| 1750 | END 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 | ||
| 1753 | access the markers. | ||
| 1754 | |||
| 1755 | Certain characters in text values are required to be escaped by | ||
| 1756 | the iCalendar standard. These characters should NOT be | ||
| 1757 | pre-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 | ||
| 1760 | printing 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 | |||
| 1792 | The 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 | |||
| 1801 | The parsed and printed representations are the same: a URI string. | ||
| 1802 | Typically, this should be a \"mailto:\" URI. | ||
| 1803 | |||
| 1804 | RFC5545 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 | |||
| 1808 | Since it is unclear whether there are Calendar User Address values | ||
| 1809 | which are not used to address email, this type does not enforce the use | ||
| 1810 | of the mailto: scheme, but be prepared for problems if you create | ||
| 1811 | values 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. | ||
| 1819 | S 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 | |||
| 1843 | When printed, a sign followed by a string of digits, like +HHMM | ||
| 1844 | or -HHMMSS. When read, an integer representing the number of | ||
| 1845 | seconds offset from UTC. This representation is for compatibility | ||
| 1846 | with `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. | ||
| 1863 | If 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. | ||
| 1872 | S should have already been matched against the regex for TYPE and | ||
| 1873 | the match data should be available to this function. Returns a | ||
| 1874 | syntax node of type TYPE containing the read value. | ||
| 1875 | |||
| 1876 | If TYPE accepts a list of values, S will be split on the list | ||
| 1877 | separator 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. | ||
| 1902 | TYPE should be a type symbol for an iCalendar parameter type. | ||
| 1903 | This function expects point to be at the start of the value | ||
| 1904 | string, after the parameter name and the equals sign. Returns a | ||
| 1905 | syntax 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. | ||
| 1941 | Point should be at the \";\" at the start of the first parameter. | ||
| 1942 | Returns a list of parameters, which may be nil if none are present. | ||
| 1943 | After parsing, point is at the end of the parameter string and the | ||
| 1944 | start 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. | ||
| 1970 | NODE should be a syntax node whose type is an iCalendar | ||
| 1971 | parameter 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. | ||
| 2005 | Returns 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 | |||
| 2046 | This is a comma-separated list of quoted `icalendar-cal-address' URIs, | ||
| 2047 | typically specified on the `icalendar-attendee' property. The users in | ||
| 2048 | this list have delegated their participation to the user which is | ||
| 2049 | the 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 | |||
| 2058 | This is a comma-separated list of quoted `icalendar-cal-address' URIs, | ||
| 2059 | typically specified on the `icalendar-attendee' property. The users in | ||
| 2060 | this list have been delegated to participate by the user which is | ||
| 2061 | the 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 | |||
| 2070 | This parameter may be specified on properties with a | ||
| 2071 | `icalendar-cal-address' value type. It is a quoted URI which specifies | ||
| 2072 | a reference to a directory entry associated with the calendar | ||
| 2073 | user 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 | |||
| 2081 | If \"BASE64\", the property value is base64-encoded binary data. | ||
| 2082 | This parameter must be specified if the `icalendar-valuetypeparam' | ||
| 2083 | is \"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 | |||
| 2098 | Specifies the media type of the object referenced in the property value, | ||
| 2099 | for example \"text/plain\" or \"text/html\". | ||
| 2100 | Valid media types are defined in RFC4288; see | ||
| 2101 | URL `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 | |||
| 2108 | RFC5545 gives the following meanings to the values: | ||
| 2109 | |||
| 2110 | FREE: the time interval is free for scheduling. | ||
| 2111 | BUSY: the time interval is busy because one or more events have | ||
| 2112 | been scheduled for that interval. | ||
| 2113 | BUSY-UNAVAILABLE: the time interval is busy and the interval | ||
| 2114 | can not be scheduled. | ||
| 2115 | BUSY-TENTATIVE: the time interval is busy because one or more | ||
| 2116 | events have been tentatively scheduled for that interval. | ||
| 2117 | Other 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 | |||
| 2134 | This parameter specifies the language of the property value as a | ||
| 2135 | language tag, for example \"en-US\" for US English or \"no\" for | ||
| 2136 | Norwegian. Valid language tags are defined in RFC5646; see | ||
| 2137 | URL `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 | |||
| 2144 | This is a comma-separated list of quoted `icalendar-cal-address' | ||
| 2145 | values. These are addresses of groups or lists of which the user | ||
| 2146 | in 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 | |||
| 2155 | The value specifies the participation status of the calendar user | ||
| 2156 | in the property value. They have different interpretations | ||
| 2157 | depending on whether they occur in a VEVENT, VTODO or VJOURNAL | ||
| 2158 | component. RFC5545 gives the values the following meanings: | ||
| 2159 | |||
| 2160 | NEEDS-ACTION (all): needs action by the user | ||
| 2161 | ACCEPTED (all): accepted by the user | ||
| 2162 | DECLINED (all): declined by the user | ||
| 2163 | TENTATIVE (VEVENT, VTODO): tentatively accepted by the user | ||
| 2164 | DELEGATED (VEVENT, VTODO): delegated by the user | ||
| 2165 | COMPLETED (VTODO): completed at the `icalendar-date-time' in the | ||
| 2166 | VTODO's `icalendar-completed' property | ||
| 2167 | IN-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 | |||
| 2186 | Specifies the effective range of recurrence instances of the property's value. | ||
| 2187 | The value \"THISANDFUTURE\" is the only value compliant with RFC5545; | ||
| 2188 | legacy 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 | |||
| 2196 | This parameter may be specified on properties whose values give | ||
| 2197 | an alarm trigger as an `icalendar-duration'. If the parameter | ||
| 2198 | value is \"START\" (the default), the alarm triggers relative to | ||
| 2199 | the 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 | |||
| 2207 | This parameter specifies a hierarchical relationship between the | ||
| 2208 | calendar component referenced in a `icalendar-related-to' | ||
| 2209 | property and the calendar component in which it occurs. | ||
| 2210 | \"PARENT\" means the referenced component is superior to this | ||
| 2211 | one, \"CHILD\" that the referenced component is subordinate to | ||
| 2212 | this 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 | |||
| 2226 | This parameter specifies the participation role of the calendar | ||
| 2227 | user in the property value. RFC5545 gives the parameter values | ||
| 2228 | the following meanings: | ||
| 2229 | CHAIR: chair of the calendar entity | ||
| 2230 | REQ-PARTICIPANT (default): user's participation is required | ||
| 2231 | OPT-PARTICIPANT: user's participation is optional | ||
| 2232 | NON-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 | |||
| 2248 | This parameter is an `icalendar-boolean' which specifies whether | ||
| 2249 | the calendar user in the property value is expected to reply to | ||
| 2250 | the 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 | |||
| 2258 | This parameter specifies a calendar user that is acting on behalf | ||
| 2259 | of 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 | |||
| 2273 | This parameter identifies the VTIMEZONE component in the calendar | ||
| 2274 | which should be used to interpret the time value given in the | ||
| 2275 | property. The value of this parameter must be equal to the value | ||
| 2276 | of the TZID property in that VTIMEZONE component; there must be | ||
| 2277 | exactly one such component for every unique value of this | ||
| 2278 | parameter 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. | ||
| 2284 | S should contain the printed representation of a value type in a \"VALUE=...\" | ||
| 2285 | property parameter. If S represents a known type in `icalendar-value-types', | ||
| 2286 | it 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. | ||
| 2294 | TYPE should be an iCalendar type symbol naming a known value type | ||
| 2295 | defined with `icalendar-define-type', or a string naming an | ||
| 2296 | unknown type. If it is a symbol, return the associated printed | ||
| 2297 | representation for the type from `icalendar-value-types'. | ||
| 2298 | Otherwise 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 | |||
| 2306 | When read, if the type named by the parameter is a known value | ||
| 2307 | type in `icalendar-value-types', it is represented as a type | ||
| 2308 | symbol for that value type. If it is an unknown value type, it is | ||
| 2309 | represented as a string. When printed, a string is returned | ||
| 2310 | unchanged; a type symbol is printed as the associated name in | ||
| 2311 | `icalendar-value-types'. | ||
| 2312 | |||
| 2313 | This is not a type defined by RFC5545; it is defined here to | ||
| 2314 | facilitate 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 | |||
| 2344 | This parameter is used to specify the value type of the | ||
| 2345 | containing property's value, if it is not of the default value | ||
| 2346 | type." | ||
| 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 | |||
| 2353 | This is not a parameter type defined by RFC5545; it represents | ||
| 2354 | parameters with an unknown name (matching rx `icalendar-param-name') | ||
| 2355 | whose values must be parsed and preserved but not further | ||
| 2356 | interpreted." | ||
| 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 | |||
| 2378 | TYPE should be a type symbol for an iCalendar property type | ||
| 2379 | defined with `icalendar-define-property'. The property value is | ||
| 2380 | assumed to be of TYPE's default value type, unless an | ||
| 2381 | `icalendar-valuetypeparam' parameter appears in PARAMS, in which | ||
| 2382 | case a value of that type will be read. S should have already | ||
| 2383 | been matched against TYPE's value regex and the match data should | ||
| 2384 | be available to this function. Returns a property syntax node of | ||
| 2385 | type TYPE containing the read value and the list of PARAMS. | ||
| 2386 | |||
| 2387 | If TYPE accepts lists of values, they will be split from S on the | ||
| 2388 | list 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. | ||
| 2406 | This function expects point to be at the start of the value | ||
| 2407 | expression, after \"PROPERTY-NAME[PARAM...]:\". Returns a syntax | ||
| 2408 | node of type TYPE containing the parsed value and the list of | ||
| 2409 | PARAMS." | ||
| 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 | |||
| 2484 | If the type of PROPERTY-NODE's value is not the same as its | ||
| 2485 | default-type, check that its parameter list contains an | ||
| 2486 | `icalendar-valuetypeparam' specifying that type as the type for | ||
| 2487 | the value. If not, add such a parameter to PROPERTY-NODE's list | ||
| 2488 | of parameters. Returns the possibly-modified PROPERTY-NODE. | ||
| 2489 | |||
| 2490 | If the parameter list already contains a value type parameter for | ||
| 2491 | a type other than the property value's type, an | ||
| 2492 | `icalendar-validation-error' is signaled. | ||
| 2493 | |||
| 2494 | If PROPERTY's value is a list, the type of the first element will | ||
| 2495 | be assumed to be the type for all the values in the list. If the | ||
| 2496 | list 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. | ||
| 2536 | PARAMS should be a list of parameter nodes. The type symbol specified by | ||
| 2537 | the 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 | |||
| 2548 | Point should be at the beginning of a property line; LIMIT should be the | ||
| 2549 | position at the end of the line. | ||
| 2550 | |||
| 2551 | Returns a syntax node for the property. After parsing, point is at the | ||
| 2552 | beginning 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 | |||
| 2611 | This property specifies the time scale of an | ||
| 2612 | `icalendar-vcalendar' object. The only scale defined by RFC5545 | ||
| 2613 | is \"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 | |||
| 2623 | When an `icalendar-vcalendar' is sent in a MIME message, this property | ||
| 2624 | specifies the semantics of the request in the message: e.g. it is | ||
| 2625 | a request to publish the calendar object, or a reply to an | ||
| 2626 | invitation. This property and the MIME message's \"method\" | ||
| 2627 | parameter value must be the same. | ||
| 2628 | |||
| 2629 | RFC5545 does not define any methods, but RFC5546 does; see | ||
| 2630 | URL `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 | |||
| 2639 | This property identifies the program that created an | ||
| 2640 | `icalendar-vcalendar' object. It must be specified exactly once in a | ||
| 2641 | calendar object. Its value should be a globally unique identifier for | ||
| 2642 | the program. RFC5545 suggests using an ISO \"Formal Public Identifier\"; | ||
| 2643 | see 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 | |||
| 2651 | This property specifies the version number of the iCalendar | ||
| 2652 | specification to which an `icalendar-vcalendar' object conforms, | ||
| 2653 | and must be specified exactly once in a calendar object. It is | ||
| 2654 | either the string \"2.0\" or a string like MIN;MAX specifying | ||
| 2655 | minimum and maximum versions of future revisions of the | ||
| 2656 | specification." | ||
| 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 | |||
| 2670 | This property specifies a file attached to an iCalendar | ||
| 2671 | component, either via a URI, or as encoded binary data. In | ||
| 2672 | `icalendar-valarm' components, it is used to specify the | ||
| 2673 | notification 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. | ||
| 2688 | Checks that NODE has a correct `icalendar-encodingparam' and | ||
| 2689 | `icalendar-valuetypeparam' if its value is an `icalendar-binary'. | ||
| 2690 | |||
| 2691 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 2692 | ATTACH 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 | |||
| 2717 | This property lists categories or subtypes of an iCalendar | ||
| 2718 | component for e.g. searching or filtering. The categories can be | ||
| 2719 | any `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 | |||
| 2729 | This property specifies the scope of access that the calendar | ||
| 2730 | owner 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 | |||
| 2749 | This property can be specified multiple times in calendar components, | ||
| 2750 | and 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 | |||
| 2759 | This property should be a longer, more complete description of | ||
| 2760 | the calendar component than is contained in the | ||
| 2761 | `icalendar-summary' property. In a `icalendar-vjournal' | ||
| 2762 | component, it is used to capture a journal entry, and may be | ||
| 2763 | specified multiple times. Otherwise it may only be specified | ||
| 2764 | once. In an `icalendar-valarm' component, it contains the | ||
| 2765 | notification 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 | |||
| 2787 | This is not a type defined by RFC5545; it is defined here to | ||
| 2788 | facilitate parsing the `icalendar-geo' property. When printed, it | ||
| 2789 | is represented as a pair of `icalendar-float' values separated by | ||
| 2790 | a semicolon, like LATITUDE;LONGITUDE. When read, it is a dotted | ||
| 2791 | pair 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 | |||
| 2800 | Both values are floats representing a number of degrees. The | ||
| 2801 | latitude value is north of the equator if positive, and south of | ||
| 2802 | the equator if negative. The longitude value is east of the prime | ||
| 2803 | meridian 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 | |||
| 2811 | This property describes the intended location or venue of a | ||
| 2812 | component, 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 | ||
| 2815 | structured 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 | |||
| 2825 | This property describes progress toward the completion of an | ||
| 2826 | `icalendar-vtodo' component. It can appear at most once in such a | ||
| 2827 | component. If this TODO is assigned to multiple people, the value | ||
| 2828 | represents the completion state for each person individually. The | ||
| 2829 | value should be between 0 and 100 (though this is not currently | ||
| 2830 | enforced 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 | |||
| 2839 | This property describes the priority of a component. 0 means an | ||
| 2840 | undefined priority. Other values range from 1 (highest priority) | ||
| 2841 | to 9 (lowest priority). See RFC5545 for suggestions on how to | ||
| 2842 | represent 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 | |||
| 2851 | This property is a list of `icalendar-text' values that describe | ||
| 2852 | any resources required or foreseen for the activity represented | ||
| 2853 | by 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 | |||
| 2863 | This is not a real type defined by RFC5545; it is defined here to | ||
| 2864 | facilitate 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 | |||
| 2879 | This property is a keyword used by an Organizer to inform | ||
| 2880 | Attendees 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 | ||
| 2884 | at 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 | |||
| 2892 | This property provides a short, one-line description of a | ||
| 2893 | component for display purposes. In an EMAIL `icalendar-valarm', | ||
| 2894 | it is used as the subject of the email. A longer description of | ||
| 2895 | the component can be provided in the `icalendar-description' | ||
| 2896 | property." | ||
| 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. | ||
| 2906 | Checks 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 | |||
| 2923 | This property is a timestamp that records the date and time when | ||
| 2924 | an `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 | |||
| 2933 | This property's value specifies when an `icalendar-vevent' or | ||
| 2934 | `icalendar-freebusy' ends. Its value must be of the same type as | ||
| 2935 | the value of the component's corresponding `icalendar-dtstart' | ||
| 2936 | property. The value is a non-inclusive bound, i.e., the value of | ||
| 2937 | this property must be the first time or date *after* the end of | ||
| 2938 | the 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 | |||
| 2951 | This property specifies the date (and possibly time) by which an | ||
| 2952 | `icalendar-todo' item is expected to be completed, i.e., its | ||
| 2953 | deadline. If the component also has an `icalendar-dtstart' | ||
| 2954 | property, the two properties must have the same value type, and | ||
| 2955 | the value of the DTSTART property must be earlier than the value | ||
| 2956 | of 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 | |||
| 2969 | This 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 | ||
| 2972 | block. In `icalendar-standard' and `icalendar-daylight' | ||
| 2973 | sub-components, it defines the start time of a time zone | ||
| 2974 | specification. | ||
| 2975 | |||
| 2976 | It is required in any component with an `icalendar-rrule' | ||
| 2977 | property, and in any `icalendar-vevent' component contained in a | ||
| 2978 | calendar that does not have a `icalendar-method' property. | ||
| 2979 | |||
| 2980 | Its value must be of the same type as the value of the | ||
| 2981 | component's corresponding `icalendar-dtend' property. In an | ||
| 2982 | `icalendar-vtodo' component, it must also be of the same type as | ||
| 2983 | the 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 | |||
| 2996 | This property specifies a duration of time for a component. | ||
| 2997 | In an `icalendar-vevent', it can be used to implicitly specify | ||
| 2998 | the end of the event, instead of an explicit `icalendar-dtend'. | ||
| 2999 | In an `icalendar-vtodo', it can likewise be used to implicitly specify | ||
| 3000 | the due date, instead of an explicit `icalendar-due'. | ||
| 3001 | In an `icalendar-valarm', it used to specify the delay period | ||
| 3002 | before the alarm repeats. | ||
| 3003 | |||
| 3004 | If a related `icalendar-dtstart' property has an `icalendar-date' | ||
| 3005 | value, 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 | |||
| 3013 | This property specifies a list of periods of free or busy time in | ||
| 3014 | an `icalendar-vfreebusy' component. Whether it specifies free or | ||
| 3015 | busy times is determined by its `icalendar-fbtype' parameter. The | ||
| 3016 | times 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 | |||
| 3026 | Note that this property only allows two values: \"TRANSPARENT\" | ||
| 3027 | or \"OPAQUE\". An OPAQUE value means that the component consumes | ||
| 3028 | time on a calendar. TRANSPARENT means it does not, and thus is | ||
| 3029 | invisible 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 | |||
| 3042 | This property specifies the unique identifier for a time zone in | ||
| 3043 | an `icalendar-vtimezone' component, and is a required property of | ||
| 3044 | that component. This is an identifier that `icalendar-tzidparam' | ||
| 3045 | parameters 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 | |||
| 3053 | This 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 | |||
| 3063 | This property specifies the time zone offset that is in use | ||
| 3064 | *prior to* this time zone observance. It is used to calculate the | ||
| 3065 | absolute time at which the observance takes place. It is a | ||
| 3066 | required property of an `icalendar-vtimezone' component. Positive | ||
| 3067 | numbers indicate time east of the prime meridian (ahead of UTC). | ||
| 3068 | Negative numbers indicate time west of the prime meridian (behind | ||
| 3069 | UTC)." | ||
| 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 | |||
| 3077 | This property specifies the time zone offset that is in use *in* | ||
| 3078 | this time zone observance. It is used to calculate the absolute | ||
| 3079 | time at which a new observance takes place. It is a required | ||
| 3080 | property of `icalendar-standard' and `icalendar-daylight' | ||
| 3081 | components. Positive numbers indicate time east of the prime | ||
| 3082 | meridian (ahead of UTC). Negative numbers indicate time west of | ||
| 3083 | the 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 | |||
| 3091 | This 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 | |||
| 3102 | This property specfies a participant in a `icalendar-vevent', | ||
| 3103 | `icalendar-vtodo', or `icalendar-valarm'. It is required when the | ||
| 3104 | containing component represents event, task, or notification for | ||
| 3105 | a *group* of people, but not for components that simply represent | ||
| 3106 | these items in a single user's calendar (in that case, it should | ||
| 3107 | not be specified). The property can be specified multiple times, | ||
| 3108 | once for each participant in the event or task. In an | ||
| 3109 | EMAIL-category VALARM component, this property specifies the | ||
| 3110 | address of the user(s) who should receive the notification email. | ||
| 3111 | |||
| 3112 | The parameters `icalendar-roleparam', `icalendar-partstatparam', | ||
| 3113 | `icalendar-rsvpparam', `icalendar-delfromparam', and | ||
| 3114 | `icalendar-deltoparam' are especially relevant for further | ||
| 3115 | specifying the roles of each participant in the containing | ||
| 3116 | component." | ||
| 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 | |||
| 3135 | This 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 | |||
| 3146 | This property specifies the organizer of a group-scheduled | ||
| 3147 | `icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'. | ||
| 3148 | It is required in those components if they represent a calendar | ||
| 3149 | entity with multiple participants. In an `icalendar-vfreebusy' | ||
| 3150 | component, it used to specify the user requesting free or busy | ||
| 3151 | time, or the user who published the calendar that the free/busy | ||
| 3152 | information 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 | |||
| 3164 | This property is used together with the `icalendar-uid' and | ||
| 3165 | `icalendar-sequence' properties to identify a specific instance | ||
| 3166 | of a recurring `icalendar-vevent', `icalendar-vtodo', or | ||
| 3167 | `icalendar-vjournal' component. The property value is the | ||
| 3168 | original value of the `icalendar-dtstart' property of the | ||
| 3169 | recurrence instance. Its value must have the same type as that | ||
| 3170 | property'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 | |||
| 3185 | This property specifies the `icalendar-uid' value of a different, | ||
| 3186 | related calendar component. It can be specified on an | ||
| 3187 | `icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal' | ||
| 3188 | component. An `icalendar-reltypeparam' can be used to specify the | ||
| 3189 | relationship 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 | |||
| 3198 | This 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 | |||
| 3209 | This property specifies a globally unique identifier for the | ||
| 3210 | containing component, and is required in an `icalendar-vevent', | ||
| 3211 | `icalendar-vtodo', `icalendar-vjournal', or `icalendar-vfreebusy' | ||
| 3212 | component. | ||
| 3213 | |||
| 3214 | RFC5545 requires that the program generating the UID guarantee | ||
| 3215 | that it be unique, and recommends generating it in a format which | ||
| 3216 | includes a timestamp on the left hand side of an '@' character, | ||
| 3217 | and the domain name or IP address of the host on the right-hand | ||
| 3218 | side." | ||
| 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 | |||
| 3228 | This property defines a list of exceptions to a recurrence rule | ||
| 3229 | in an `icalendar-vevent', `icalendar-todo', `icalendar-vjournal', | ||
| 3230 | `icalendar-standard', or `icalendar-daylight' component. Together | ||
| 3231 | with the `icalendar-dtstart', `icalendar-rrule', and | ||
| 3232 | `icalendar-rdate' properties, it defines the recurrence set of | ||
| 3233 | the 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 | |||
| 3247 | This 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. | ||
| 3250 | Together with the `icalendar-dtstart', `icalendar-rrule', and | ||
| 3251 | `icalendar-exdate' properties, it defines the recurrence set of | ||
| 3252 | the 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 | |||
| 3267 | This property defines a rule or repeating pattern for the dates | ||
| 3268 | and 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' | ||
| 3272 | properties, 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 | |||
| 3283 | This property defines the action to be taken when the containing | ||
| 3284 | `icalendar-valarm' component is triggered. It is a required | ||
| 3285 | property 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 | |||
| 3303 | This property specifies the number of times an `icalendar-valarm' | ||
| 3304 | should repeat after it is initially triggered. This property, | ||
| 3305 | along with the `icalendar-duration' property, is required if the | ||
| 3306 | alarm 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 | |||
| 3315 | This property specifies when an `icalendar-valarm' should | ||
| 3316 | trigger. If the value is an `icalendar-dur-value', it represents | ||
| 3317 | a time of that duration relative to the start or end of a related | ||
| 3318 | `icalendar-vevent' or `icalendar-vtodo'. Whether the trigger | ||
| 3319 | applies to the start time or end time of the related component | ||
| 3320 | can be specified with the `icalendar-trigrelparam' parameter. A | ||
| 3321 | positive duration value triggers after the start or end of the | ||
| 3322 | related component; a negative duration value triggers before. | ||
| 3323 | |||
| 3324 | If the value is an `icalendar-date-time', it must be in UTC | ||
| 3325 | format, 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. | ||
| 3337 | Checks that NODE has valid parameters depending on the type of its value. | ||
| 3338 | |||
| 3339 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 3340 | TRIGGER 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 | |||
| 3370 | This property specifies the date and time when the calendar user | ||
| 3371 | initially created an `icalendar-vevent', `icalendar-vtodo', or | ||
| 3372 | `icalendar-vjournal' in the calendar database. The value must be | ||
| 3373 | in 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 | |||
| 3381 | In an `icalendar-vevent', `icalendar-vtodo', | ||
| 3382 | `icalendar-vjournal', or `icalendar-vfreebusy', this property | ||
| 3383 | specifies the date and time when the calendar user last revised | ||
| 3384 | the component's data in the calendar database. (In this case, it | ||
| 3385 | is equivalent to the `icalendar-last-modified' property.) | ||
| 3386 | |||
| 3387 | If this property is specified on an `icalendar-vcalendar' object | ||
| 3388 | which contains an `icalendar-method' property, it specifies the | ||
| 3389 | date and time when that instance of the calendar object was | ||
| 3390 | created. In this case, it differs from the `icalendar-creation' | ||
| 3391 | and `icalendar-last-modified' properties: whereas those specify | ||
| 3392 | the time the underlying data was created and last modified in the | ||
| 3393 | calendar database, this property specifies when the calendar | ||
| 3394 | object *representing* that data was created. | ||
| 3395 | |||
| 3396 | The 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 | |||
| 3404 | This property specifies when the data in an `icalendar-vevent', | ||
| 3405 | `icalendar-vtodo', `icalendar-vjournal', or `icalendar-vtimezone' | ||
| 3406 | was 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 | |||
| 3414 | This property specifies the number of the current revision in a | ||
| 3415 | sequence of revisions in an `icalendar-vevent', | ||
| 3416 | `icalendar-vtodo', or `icalendar-vjournal' component. It starts | ||
| 3417 | at 0 and should be incremented monotonically every time the | ||
| 3418 | Organizer makes a significant revision to the calendar data that | ||
| 3419 | component 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 | |||
| 3430 | This property type corresponds to the IANA Properties and | ||
| 3431 | Non-Standard Properties defined in RFC5545; it represents | ||
| 3432 | properties with an unknown name (matching rx | ||
| 3433 | `icalendar-iana-token' or `icalendar-x-name') whose values must | ||
| 3434 | be parsed and preserved but not further interpreted. Its value | ||
| 3435 | may be set to any type with the `icalendar-valuetypeparam' | ||
| 3436 | parameter." | ||
| 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. | ||
| 3448 | S 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 | |||
| 3476 | When read, a list (CODE DESCRIPTION EXCEPTION). CODE is a hierarchical | ||
| 3477 | numerical 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 | ||
| 3482 | DESCRIPTION is a longer description of the request status, also a string. | ||
| 3483 | EXCEPTION (which may be nil) is textual data describing an error. | ||
| 3484 | |||
| 3485 | When printed, the three elements are separated by semicolons, like | ||
| 3486 | CODE;DESCRIPTION;EXCEPTION | ||
| 3487 | or | ||
| 3488 | CODE;DESCRIPTION | ||
| 3489 | if EXCEPTION is nil. | ||
| 3490 | |||
| 3491 | This is not a type defined by RFC5545; it is defined here to | ||
| 3492 | facilitate 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. | ||
| 3523 | Point should be at the start of the component, i.e., at the start | ||
| 3524 | of a line that looks like \"BEGIN:[COMPONENT-NAME]\". After parsing, | ||
| 3525 | point 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. | ||
| 3589 | Point should be at the beginning of a line which begins a | ||
| 3590 | component 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 | |||
| 3648 | This component contains properties which describe an event, such | ||
| 3649 | as its start and end time (`icalendar-dtstart' and | ||
| 3650 | `icalendar-dtend') and a summary (`icalendar-summary') and | ||
| 3651 | description (`icalendar-description'). It may also contain | ||
| 3652 | `icalendar-valarm' components as subcomponents which describe | ||
| 3653 | reminder notifications related to the event. Event components can | ||
| 3654 | only be direct children of an `icalendar-vcalendar'; they cannot | ||
| 3655 | be 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 | |||
| 3695 | NODE 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' | ||
| 3698 | properties satisfy the requirements imposed by this rule. | ||
| 3699 | |||
| 3700 | This function is called by the additional validator functions for | ||
| 3701 | component nodes (e.g. `icalendar-vevent-validator'); it is not normally | ||
| 3702 | necessary 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. | ||
| 3810 | Checks that NODE has does not have both `icalendar-duration' and | ||
| 3811 | `icalendar-dtend' properties, and calls `icalendar-rrule-validator'. | ||
| 3812 | |||
| 3813 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 3814 | VEVENT 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 | |||
| 3845 | This component contains properties which describe a to-do item or | ||
| 3846 | task, such as its due date (`icalendar-due') and a summary | ||
| 3847 | (`icalendar-summary') and description (`icalendar-description'). | ||
| 3848 | It may also contain `icalendar-valarm' components as | ||
| 3849 | subcomponents which describe reminder notifications related to | ||
| 3850 | the task. To-do components can only be direct children of an | ||
| 3851 | `icalendar-vcalendar'; they cannot be subcomponents of any other | ||
| 3852 | component." | ||
| 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. | ||
| 3890 | Checks that NODE has conformant `icalendar-due', | ||
| 3891 | `icalendar-duration', and `icalendar-dtstart' properties, and calls | ||
| 3892 | `icalendar-rrule-validator'. | ||
| 3893 | |||
| 3894 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 3895 | VTODO 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 | |||
| 3916 | This component contains properties which describe a journal | ||
| 3917 | entry, which might be any longer-form data (e.g., meeting notes, | ||
| 3918 | a diary entry, or information needed to complete a task). It can | ||
| 3919 | be associated with an `icalendar-vevent' or `icalendar-vtodo' via | ||
| 3920 | the `icalendar-related-to' property. A journal entry does not | ||
| 3921 | take up time in a calendar, and plays no role in searches for | ||
| 3922 | free or busy time. Journal components can only be direct children | ||
| 3923 | of `icalendar-vcalendar'; they cannot be subcomponents of any | ||
| 3924 | other 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 | ||
| 3953 | or response for such blocks. | ||
| 3954 | |||
| 3955 | The free/busy information is represented by the | ||
| 3956 | `icalendar-freebusy' property (which may be given more than once) | ||
| 3957 | and the related `icalendar-fbtype' parameter. Note that | ||
| 3958 | recurrence properties (`icalendar-rrule', `icalendar-rdate', and | ||
| 3959 | `icalendar-exdate') are NOT permitted in this component. | ||
| 3960 | |||
| 3961 | When used to publish blocks of free/busy time in a user's | ||
| 3962 | schedule, the `icalendar-organizer' property specifies the user. | ||
| 3963 | |||
| 3964 | When used to request free/busy time in a user's schedule, or to | ||
| 3965 | respond to such a request, the `icalendar-attendee' property | ||
| 3966 | specifies the user whose time is being requested, and the | ||
| 3967 | `icalendar-organizer' property specifies the user making the | ||
| 3968 | request. | ||
| 3969 | |||
| 3970 | Free/busy components can only be direct children | ||
| 3971 | of `icalendar-vcalendar'; they cannot be subcomponents of any | ||
| 3972 | other 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 | |||
| 3991 | A time zone is identified by an `icalendar-tzid' property, which | ||
| 3992 | is required in this component. Times in other calendar components | ||
| 3993 | can be specified in local time in this time zone with the | ||
| 3994 | `icalendar-tzidparam' parameter. An `icalendar-vcalendar' object | ||
| 3995 | must contain exactly one `icalendar-vtimezone' component for each | ||
| 3996 | unique time zone identifier used in the calendar. | ||
| 3997 | |||
| 3998 | Besides the time zone identifier, a time zone component must | ||
| 3999 | contain at least one `icalendar-standard' or `icalendar-daylight' | ||
| 4000 | subcomponent, which describe the observance of standard or | ||
| 4001 | daylight time in the time zone, including the dates of the | ||
| 4002 | observance 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. | ||
| 4014 | Checks that NODE has at least one `icalendar-standard' or | ||
| 4015 | `icalendar-daylight' child. | ||
| 4016 | |||
| 4017 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 4018 | VTIMEZONE 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 | |||
| 4033 | The observance has a start time, specified by an | ||
| 4034 | `icalendar-dtstart' property, which is required in this component | ||
| 4035 | and must be in *local* time format. The observance may have a | ||
| 4036 | recurring onset (e.g. each year on a particular day or date) | ||
| 4037 | described by the `icalendar-rrule' and `icalendar-rdate' | ||
| 4038 | properties. An end date for the observance, if there is one, must | ||
| 4039 | be specified in the UNTIL clause of the `icalendar-rrule' in UTC | ||
| 4040 | time. | ||
| 4041 | |||
| 4042 | The offset from UTC time when the observance begins is specified | ||
| 4043 | in the `icalendar-tzoffsetfrom' property, which is required. The | ||
| 4044 | offset from UTC time while the observance is in effect is | ||
| 4045 | specified by the `icalendar-tzoffsetto' property, which is also | ||
| 4046 | required. A common identifier for the time zone observance can be | ||
| 4047 | specified in the `icalendar-tzname' property. Other explanatory | ||
| 4048 | comments can be provided in `icalendar-comment'. | ||
| 4049 | |||
| 4050 | This component must be a direct child of an `icalendar-vtimezone' | ||
| 4051 | component 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 | |||
| 4066 | The observance has a start time, specified by an | ||
| 4067 | `icalendar-dtstart' property, which is required in this component | ||
| 4068 | and must be in *local* time format. The observance may have a | ||
| 4069 | recurring onset (e.g. each year on a particular day or date) | ||
| 4070 | described by the `icalendar-rrule' and `icalendar-rdate' | ||
| 4071 | properties. An end date for the observance, if there is one, must | ||
| 4072 | be specified in the UNTIL clause of the `icalendar-rrule' in UTC | ||
| 4073 | time. | ||
| 4074 | |||
| 4075 | The offset from UTC time when the observance begins is specified | ||
| 4076 | in the `icalendar-tzoffsetfrom' property, which is required. The | ||
| 4077 | offset from UTC time while the observance is in effect is | ||
| 4078 | specified by the `icalendar-tzoffsetto' property, which is also | ||
| 4079 | required. A common identifier for the time zone observance can be | ||
| 4080 | specified in the `icalendar-tzname' property. Other | ||
| 4081 | explanatory comments can be provided in `icalendar-comment'. | ||
| 4082 | |||
| 4083 | This component must be a direct child of an `icalendar-vtimezone' | ||
| 4084 | component 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 | |||
| 4099 | An alarm is a notification or reminder for an event or task. The | ||
| 4100 | type of notification is determined by this component's | ||
| 4101 | `icalendar-action' property: it may be an AUDIO, DISPLAY, or | ||
| 4102 | EMAIL notification. | ||
| 4103 | If it is an audio alarm, it can include an | ||
| 4104 | `icalendar-attach' property specifying the audio to be rendered. | ||
| 4105 | If it is a DISPLAY alarm, it must include an `icalendar-description' | ||
| 4106 | property containing the text to be displayed. | ||
| 4107 | If it is an EMAIL alarm, it must include both an | ||
| 4108 | `icalendar-summary' and an `icalendar-description', which specify | ||
| 4109 | the subject and body of the email, and one or more | ||
| 4110 | `icalendar-attendee' properties, which specify the recipients. | ||
| 4111 | |||
| 4112 | The required `icalendar-trigger' property specifies when the | ||
| 4113 | alarm triggers. If the alarm repeats, then `icalendar-duration' | ||
| 4114 | and `icalendar-repeat' properties are also both required. | ||
| 4115 | |||
| 4116 | This component must occur as a direct child of an | ||
| 4117 | `icalendar-vevent' or `icalendar-vtodo' component, and cannot | ||
| 4118 | contain 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. | ||
| 4131 | Checks that NODE has the right properties corresponding to its | ||
| 4132 | `icalendar-action' type, e.g., that an EMAIL alarm has a | ||
| 4133 | subject (`icalendar-summary') and recipients (`icalendar-attendee'). | ||
| 4134 | |||
| 4135 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 4136 | VALARM 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 | |||
| 4202 | This component type corresponds to the IANA and X-name components | ||
| 4203 | allowed by RFC5545 sec. 3.6; it represents components with an | ||
| 4204 | unknown name (matching rx `icalendar-iana-token' or | ||
| 4205 | `icalendar-x-name') which must be parsed and preserved but not | ||
| 4206 | further 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 | |||
| 4217 | This is the top-level data structure defined by RFC5545. A | ||
| 4218 | VCALENDAR must contain the calendar properties `icalendar-prodid' | ||
| 4219 | and `icalendar-version', and may contain the calendar properties | ||
| 4220 | `icalendar-method' and `icalendar-calscale'. | ||
| 4221 | |||
| 4222 | It must also contain at least one VEVENT, VTODO, VJOURNAL, | ||
| 4223 | VFREEBUSY, or other component, and for every unique | ||
| 4224 | `icalendar-tzidparam' value appearing in a property within these | ||
| 4225 | components, 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 | |||
| 4252 | Checks 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 | |||
| 4256 | This function is called by `icalendar-ast-node-valid-p' for | ||
| 4257 | VCALENDAR 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 | |||
| 4291 | If so, then BUFFER is a candidate for parsing with, e.g., | ||
| 4292 | `icalendar-parse-calendar'. BUFFER defaults to the current | ||
| 4293 | buffer. 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. | ||
| 4309 | Point should be at the start of the calendar object, i.e., at the start | ||
| 4310 | of a line that looks like \"BEGIN:VCALENDAR\". After parsing, point is | ||
| 4311 | at the beginning of the next line following the calendar (or end of the | ||
| 4312 | buffer). 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 | |||
| 4340 | If VCALENDAR is not a valid `icalendar-vcalendar', an | ||
| 4341 | `icalendar-validation-error' will be signaled. Any errors that arise | ||
| 4342 | during 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 | |||
| 4355 | An unfolded copy of BUFFER (see `icalendar-unfolded-buffer-from-buffer') | ||
| 4356 | will first be obtained if necessary. Parsing will begin at the first | ||
| 4357 | occurrence of \"BEGIN:VCALENDAR\" in the unfolded buffer. | ||
| 4358 | |||
| 4359 | The buffer may be tidied up by user functions before parsing begins; see | ||
| 4360 | `icalendar-pre-unfolding-hook' and `icalendar-pre-parsing-hook'. | ||
| 4361 | |||
| 4362 | If parsing is successful, the VCALENDAR object is returned. Otherwise, | ||
| 4363 | nil is returned, a warning is issued, and errors are logged in the | ||
| 4364 | buffer 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 | |||
| 4425 | If you routinely receive iCalendar data in an incorrect format, you can | ||
| 4426 | add functions to this hook which clean up that data before parsing is | ||
| 4427 | attempted. The functions in this hook will be run after the iCalendar | ||
| 4428 | data has been \"unfolded\" but before parsing begins. (If you need to | ||
| 4429 | clean up data before unfolding happens, see | ||
| 4430 | `icalendar-pre-unfolding-hook'.) | ||
| 4431 | |||
| 4432 | Each function should accept zero arguments and should perform its | ||
| 4433 | operation 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. | ||
| 4442 | This function is intended to be used from `icalendar-pre-parsing-hook', | ||
| 4443 | which 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. | ||
| 4451 | This function is intended to be used from `icalendar-pre-parsing-hook', | ||
| 4452 | which 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. | ||
| 4473 | This function is intended to be used from `icalendar-pre-parsing-hook', | ||
| 4474 | which 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 | |||
| 4671 | INDEX 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 | |||
| 4683 | Only 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 | |||
| 4750 | Returns a list (VCALENDAR INDEX), where VCALENDAR is the parsed | ||
| 4751 | `icalendar-vcalendar' syntax tree. The index can then be queried to | ||
| 4752 | retrieve components from this calendar by UID, TZID, or date; see | ||
| 4753 | `icalendar-index-get'. | ||
| 4754 | |||
| 4755 | BUFFER-OR-FILE may be a buffer or a string containing a filename; it | ||
| 4756 | defaults to the current buffer. If it is a filename, an unfolded buffer | ||
| 4757 | containing its data will be found, or created if necessary (see | ||
| 4758 | `icalendar-unfolded-buffer-from-file'). The resulting buffer must | ||
| 4759 | contain an iCalendar VCALENDAR object, which will be parsed and indexed. | ||
| 4760 | |||
| 4761 | The results of parsing and indexing are cached in buffer-local | ||
| 4762 | variables, and subsequent calls with the same BUFFER-OR-FILE will return | ||
| 4763 | the cached results as long as the buffer has not been modified in the | ||
| 4764 | meantime." | ||
| 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. | ||
| 4845 | Its `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 | |||
| 159 | INTERVALSIZE should be the total size of the interval in seconds. FREQS | ||
| 160 | should be the number of seconds between the lower bound of the interval | ||
| 161 | and the upper bound for candidate recurrences; it is the number of | ||
| 162 | seconds in the unit of time in a recurrence rule's FREQ part. The | ||
| 163 | returned 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. | ||
| 270 | See `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. | ||
| 280 | See `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. | ||
| 292 | See `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. | ||
| 304 | See `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. | ||
| 334 | See `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. | ||
| 374 | See `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. | ||
| 409 | See `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 | |||
| 430 | TARGET and DTSTART should be `icalendar-date' or `icalendar-date-time' | ||
| 431 | values. RECUR-VALUE should be an `icalendar-recur'. | ||
| 432 | |||
| 433 | The returned value is a list (LOW HIGH NEXT-LOW) which | ||
| 434 | represents the lower and upper bounds of a recurrence interval around | ||
| 435 | TARGET. For some N, LOW is equal to START + N*INTERVALSIZE units, HIGH | ||
| 436 | is equal to START + (N+1)*INTERVALSIZE units, and LOW <= TARGET < HIGH. | ||
| 437 | START here is a time derived from DTSTART depending on RECUR-VALUE's | ||
| 438 | FREQ part: the first day of the year for a \\='YEARLY rule, first day | ||
| 439 | of the month for a \\='MONTHLY rule, etc. | ||
| 440 | |||
| 441 | RECUR-VALUE's interval determines INTERVALSIZE, and its frequency | ||
| 442 | determines the units: a month for \\='MONTHLY, etc. | ||
| 443 | |||
| 444 | If VTIMEZONE is provided, it is used to set time zone information in the | ||
| 445 | returned interval bounds. Otherwise, the bounds contain no time zone | ||
| 446 | information 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 | |||
| 463 | The returned value is a list (LOW HIGH NEXT-LOW) which represent the Nth | ||
| 464 | recurrence interval after DTSTART. LOW is equal to START + | ||
| 465 | N*INTERVALSIZE units, HIGH is equal to START + (N+1)*INTERVALSIZE units, | ||
| 466 | and LOW <= TARGET < HIGH. START here is a time derived from DTSTART | ||
| 467 | depending 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 | |||
| 470 | RECUR-VALUE's interval determines INTERVALSIZE, and its frequency | ||
| 471 | determines the units: a month for \\='MONTHLY, etc. | ||
| 472 | |||
| 473 | N should be a non-negative integer. Interval 0 is the interval | ||
| 474 | containing DTSTART. DTSTART should be an `icalendar-date' or | ||
| 475 | `icalendar-date-time' value. RECUR-VALUE should be an | ||
| 476 | `icalendar-recur'. | ||
| 477 | |||
| 478 | If VTIMEZONE is provided, it is used to set time zone information in the | ||
| 479 | returned interval bounds. Otherwise, the bounds contain no time zone | ||
| 480 | information 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 | |||
| 501 | Given a recurrence interval (LOW HIGH NEXT), returns the next interval | ||
| 502 | \(NEXT HIGHER HIGHER-NEXT), where HIGHER and HIGHER-NEXT are determined | ||
| 503 | by 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 | |||
| 529 | For 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 | ||
| 531 | the frequency and interval sizes of RECUR-VALUE (see | ||
| 532 | `icalendar-recur-find-interval'). If the resulting period of time | ||
| 533 | between PREV-LOW and PREV-HIGH occurs entirely before DTSTART, then the | ||
| 534 | interval 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 | |||
| 617 | YEARDAYS should be a list of values from a recurrence rule's | ||
| 618 | BYYEARDAY=... 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 | |||
| 659 | WEEKNOS should be a list of values from a recurrence rule's | ||
| 660 | BYWEEKNO=... clause, and WEEKSTART should be the value of its | ||
| 661 | WKST=... 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 | |||
| 700 | MONTHS should be a list of values from a recurrence rule's | ||
| 701 | BYMONTH=... 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 | |||
| 734 | MONTHDAYS should be a list of values from a recurrence rule's | ||
| 735 | BYMONTHDAY=... 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 | |||
| 774 | WEEKDAYS should be a list of values from a recurrence rule's | ||
| 775 | BYDAY=... clause; see `icalendar-recur' for the possible values. | ||
| 776 | |||
| 777 | If WEEKDAYS contains pairs (DOW . OFFSET), then IN-MONTH indicates | ||
| 778 | whether OFFSET is relative to the month of the start of the interval. If | ||
| 779 | it 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 | |||
| 845 | HOURS should be a list of values from a recurrence rule's | ||
| 846 | BYHOUR=... 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 | |||
| 874 | MINUTES should be a list of values from a recurrence rule's | ||
| 875 | BYMINUTE=... 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 | |||
| 909 | SECONDS should be a list of values from a recurrence rule's | ||
| 910 | BYSECOND=... 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 | |||
| 958 | SETPOS should be a list of positive or negative integers between -366 | ||
| 959 | and 366, indicating a fixed index in a set of recurrences for *one | ||
| 960 | interval* of a recurrence set, as found in the BYSETPOS=... clause of | ||
| 961 | an `icalendar-recur'. For example, in a YEARLY recurrence rule with an | ||
| 962 | INTERVAL of 1, the SETPOS represent indices in the recurrence instances | ||
| 963 | generated for a single year. | ||
| 964 | |||
| 965 | The returned value is a closure which can be called on the list of | ||
| 966 | recurrences 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 | |||
| 987 | The resulting list of subintervals represents all times in INTERVAL | ||
| 988 | which match the BY* clauses of RECUR-VALUE except BYSETPOS, as well as | ||
| 989 | the constraints implicit in DTSTART. (For example, if there is no | ||
| 990 | BYMINUTE clause, subintervals will have the same minute value as | ||
| 991 | DTSTART.) | ||
| 992 | |||
| 993 | If specified, VTIMEZONES should be a list of `icalendar-vtimezone' | ||
| 994 | components and TZID should be the `icalendar-tzid' property value of one | ||
| 995 | of those timezones. In this case, TZID states the time zone of DTSTART, | ||
| 996 | and the offsets effective in that time zone on the dates and times of | ||
| 997 | recurrences 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 | |||
| 1098 | The returned list of recurrences contains one date-time value for each | ||
| 1099 | second 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 | |||
| 1120 | The returned list of recurrences contains one date value for each | ||
| 1121 | day 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 | |||
| 1147 | The returned list of recurrences contains all distinct values in each | ||
| 1148 | subinterval 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 | |||
| 1165 | INTERVAL should be a list (LOW HIGH NEXT) of date-times which bound a | ||
| 1166 | single recurrence interval, as returned e.g. by | ||
| 1167 | `icalendar-recur-find-interval'. (To find the recurrences in an | ||
| 1168 | arbitrary window of time, rather than between interval boundaries, see | ||
| 1169 | `icalendar-recur-recurrences-in-window'.) | ||
| 1170 | |||
| 1171 | COMPONENT should be an iCalendar component node representing a recurring | ||
| 1172 | event: it should contain at least an `icalendar-dtstart' and either an | ||
| 1173 | `icalendar-rrule' or `icalendar-rdate' property. | ||
| 1174 | |||
| 1175 | If specified, VTIMEZONE should be an `icalendar-vtimezone' component. | ||
| 1176 | In this case, the dates and times of recurrences will be computed with | ||
| 1177 | UTC offsets local to that time zone. | ||
| 1178 | |||
| 1179 | If specified, NMAX should be a positive integer containing a maximum | ||
| 1180 | number of recurrences to return from this interval. In this case, if the | ||
| 1181 | interval contains more than NMAX recurrences, only the first NMAX | ||
| 1182 | recurrences will be returned; otherwise all recurrences in the interval | ||
| 1183 | are returned. (The NMAX argument mainly exists to support recurrence | ||
| 1184 | rules with a COUNT clause; see `icalendar-recur-recurrences-to-count'.) | ||
| 1185 | |||
| 1186 | The returned list is a list of `icalendar-date' or `icalendar-date-time' | ||
| 1187 | values representing the start times of recurrences. Note that any | ||
| 1188 | values of type `icalendar-period' in COMPONENT's `icalendar-rdate' | ||
| 1189 | property (or properties) will NOT be included in the list; it is the | ||
| 1190 | callee's responsibility to handle any such values separately. | ||
| 1191 | |||
| 1192 | The computed recurrences for INTERVAL are cached in COMPONENT and | ||
| 1193 | retrieved 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 | |||
| 1289 | LOWER and UPPER may be arbitrary `icalendar-date' or | ||
| 1290 | `icalendar-date-time' values. COMPONENT should be an iCalendar component | ||
| 1291 | node representing a recurring event: it should contain at least an | ||
| 1292 | `icalendar-dtstart' and either an `icalendar-rrule' or `icalendar-rdate' | ||
| 1293 | property. | ||
| 1294 | |||
| 1295 | If specified, VTIMEZONE should be an `icalendar-vtimezone' component. | ||
| 1296 | In this case, the dates and times of recurrences will be computed with | ||
| 1297 | UTC 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 | |||
| 1347 | The return value is a list of (START END) pairs representing the start | ||
| 1348 | and end time of each recurrence of COMPONENT in the window defined by | ||
| 1349 | LOWER and UPPER. | ||
| 1350 | |||
| 1351 | In 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 | ||
| 1355 | LOWER and UPPER. Or, if the recurrence is given by an `icalendar-period' | ||
| 1356 | value in an `icalendar-rdate' property, START and END are determined by | ||
| 1357 | the 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 | |||
| 1390 | COMPONENT should be an iCalendar component node representing a recurring | ||
| 1391 | event: it should contain at least an `icalendar-dtstart' and an | ||
| 1392 | `icalendar-rrule', which must contain a COUNT=... clause. | ||
| 1393 | |||
| 1394 | Warning: this function finds *all* the recurrences in COMPONENT's | ||
| 1395 | recurrence set. If the value of COUNT is large, this can be slow. | ||
| 1396 | |||
| 1397 | If specified, VTIMEZONE should be an `icalendar-vtimezone' component. | ||
| 1398 | In this case, the dates and times of recurrences will be computed with | ||
| 1399 | UTC 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 | |||
| 1511 | Some local date-times do not exist in a given time zone. When switching | ||
| 1512 | from standard to daylight savings time, the local clock time jumps over | ||
| 1513 | a certain range of times. This function tests whether DT is one of those | ||
| 1514 | non-existent local times. | ||
| 1515 | |||
| 1516 | DT and OBS-ONSET should be `icalendar-date-time' values; OBS-ONSET | ||
| 1517 | should be the (local) time immediately at the onset of the | ||
| 1518 | OBSERVANCE. OBSERVANCE should be an `icalendar-standard' or | ||
| 1519 | `icalendar-daylight' component. | ||
| 1520 | |||
| 1521 | If this function returns t, then per RFC5545 Section 3.3.5, DT must be | ||
| 1522 | interpreted using the UTC offset in effect prior to the onset of | ||
| 1523 | OBSERVANCE. For example, at the switch from Standard to Daylight | ||
| 1524 | Savings time in US Eastern, the nonexistent time 2:30AM (Standard) must | ||
| 1525 | be 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 | |||
| 1548 | Some local date-times occur twice in a given time zone. When switching | ||
| 1549 | from daylight savings to standard time time, the local clock time is | ||
| 1550 | typically set back, so that a certain range of clock times occurs twice, | ||
| 1551 | once in daylight savings time and once in standard time. This function | ||
| 1552 | tests whether DT is one of those local times which occur twice. | ||
| 1553 | |||
| 1554 | DT and OBS-ONSET should be `icalendar-date-time' values; OBS-ONSET | ||
| 1555 | should be the (local) time immediately at the relevant onset of the | ||
| 1556 | OBSERVANCE. OBSERVANCE should be an `icalendar-standard' or | ||
| 1557 | `icalendar-daylight' component. | ||
| 1558 | |||
| 1559 | If this function returns t, then per RFC5545 Section 3.3.5, DT must be | ||
| 1560 | interpreted as the first occurrence of this clock time, i.e., in | ||
| 1561 | daylight 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 | |||
| 1584 | DT should be an `icalendar-date-time', OBSERVANCE an | ||
| 1585 | `icalendar-standard' or `icalendar-daylight', and OBS-ONSET the nearest | ||
| 1586 | onset of OBSERVANCE before DT. Returns an `icalendar-date-time' that can | ||
| 1587 | be used to update DT. | ||
| 1588 | |||
| 1589 | In most cases, the return value will contain a zone offset equal to | ||
| 1590 | OBSERVANCE's `icalendar-tzoffsetto' value. | ||
| 1591 | |||
| 1592 | However, when DT falls within a range of nonexistent times after | ||
| 1593 | OBS-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 | ||
| 1596 | with the UTC offset in effect prior to the OBS-ONSET of OBSERVANCE (see | ||
| 1597 | RFC5545 Section 3.3.5). So e.g. at the switch from Standard to Daylight | ||
| 1598 | in US Eastern, 2:30AM EST (a nonexistent time) becomes 3:30AM EDT, and | ||
| 1599 | at the switch from Daylight to Standard, 1:30AM (which occurs twice) | ||
| 1600 | becomes 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 | |||
| 1629 | VTIMEZONES should be a list of `icalendar-vtimezone' components. TZID | ||
| 1630 | should 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 | |||
| 1644 | If there is such an observance, the returned value is a list (OBSERVANCE | ||
| 1645 | ONSET). OBSERVANCE is an `icalendar-standard' or `icalendar-daylight' | ||
| 1646 | component node. ONSET is the recurrence of OBSERVANCE (an | ||
| 1647 | `icalendar-date-time') which occurs closest in time, but before, DT. | ||
| 1648 | |||
| 1649 | If there is no such observance in VTIMEZONE, the returned value is nil. | ||
| 1650 | |||
| 1651 | VTIMEZONE should be an `icalendar-vtimezone' component node. | ||
| 1652 | |||
| 1653 | DT may be an an `icalendar-date-time' or a Lisp timestamp. If it is a | ||
| 1654 | date-time, it represents a local time assumed to be in VTIMEZONE. Any | ||
| 1655 | existing offset in DT is ignored, and DT is compared with the local | ||
| 1656 | clock time at the start of each observance in VTIMEZONE to determine the | ||
| 1657 | correct observance and onset. (This is so that the correct observance | ||
| 1658 | can be found for clock times generated during recurrence rule | ||
| 1659 | calculations.) | ||
| 1660 | |||
| 1661 | If UPDATE is non-nil, the observance found will be used to update the | ||
| 1662 | offset value in DT (as a side effect) before returning the observance | ||
| 1663 | and onset. | ||
| 1664 | |||
| 1665 | If UPDATE is non-nil, NONEXISTING specifies how to handle clock times | ||
| 1666 | that do not exist in the observance (see | ||
| 1667 | `icalendar-recur-tz-nonexistent-date-time-p'). The keyword `:error' | ||
| 1668 | means to signal an \\='icalendar-tz-nonexistent-time error, without | ||
| 1669 | modifying any of the fields in DT. Otherwise, the default is to | ||
| 1670 | interpret DT using the offset from UTC before the onset of the found | ||
| 1671 | observance, and then reset the clock time in DT to the corresponding | ||
| 1672 | existing time after the onset of the observance. For example, the | ||
| 1673 | nonexisting time 2:30AM in Standard time on the day of the switch to | ||
| 1674 | Daylight time in the US Eastern time zone will be reset to 3:30AM | ||
| 1675 | Eastern Daylight time. | ||
| 1676 | |||
| 1677 | If DT is a Lisp timestamp, it represents an absolute time and | ||
| 1678 | comparisons with the onsets in VTIMEZONE are performed with absolute | ||
| 1679 | times. UPDATE and NONEXISTING have no meaning in this case and are | ||
| 1680 | ignored." | ||
| 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 | |||
| 1869 | OBSERVANCE should be an `icalendar-standard' or `icalendar-daylight' | ||
| 1870 | subcomponent of a particular `icalendar-vtimezone'. The returned value | ||
| 1871 | is 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 | |||
| 1877 | VTIMEZONE should be an `icalendar-vtimezone' component node. The correct | ||
| 1878 | observance for TS will be looked up in VTIMEZONE, TS will be decoded | ||
| 1879 | with the UTC offset of that observance, and its dst slot will be set | ||
| 1880 | based on whether the observance is an `icalendar-standard' or | ||
| 1881 | `icalendar-daylight' component. If VTIMEZONE does not have an | ||
| 1882 | observance that applies to TS, it is decoded into UTC time. | ||
| 1883 | |||
| 1884 | VTIMEZONE may also be an `icalendar-utc-offset'. In this case TS is | ||
| 1885 | decoded 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 | |||
| 1902 | DT should be an `icalendar-date-time' and VTIMEZONE should be an | ||
| 1903 | `icalendar-vtimezone'. VTIMEZONE can also be an `icalendar-utc-offset', | ||
| 1904 | in which case this value is directly set in DT's zone field (without | ||
| 1905 | changing its dst flag). The updated DT is returned. | ||
| 1906 | |||
| 1907 | This function generally sets only the zone and dst slots of DT, without | ||
| 1908 | changing the other slots; its main purpose is to adjust date-times | ||
| 1909 | generated from other date-times during recurrence rule calculations, | ||
| 1910 | where a different time zone observance may be in effect in the original | ||
| 1911 | date-time. It cannot be used to re-decode a fixed point in time into a | ||
| 1912 | different time zone; for that, see `icalendar-recur-tz-decode-time'. | ||
| 1913 | |||
| 1914 | If given, NONEXISTING is a keyword that specifies what to do if DT | ||
| 1915 | represents a clock time that does not exist according to the relevant | ||
| 1916 | observance in VTIMEZONE. The value :error means to signal an | ||
| 1917 | \\='icalendar-tz-nonexistent-time error, and nil means to reset the | ||
| 1918 | clock 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 | |||
| 1951 | VTIMEZONES should be a list of the `icalendar-vtimezone' components in | ||
| 1952 | the calendar containing NODE. NODE can be any iCalendar syntax node. If | ||
| 1953 | NODE is a property node with an `icalendar-tzidparam' parameter and an | ||
| 1954 | `icalendar-date-time' or `icalendar-period' value, the appropriate time | ||
| 1955 | zone observance for its value is looked up in VTIMEZONES, and used to | ||
| 1956 | set the zone and dst slots in its value. Otherwise, the function is | ||
| 1957 | called 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 | |||
| 1985 | DT should be an `icalendar-date' or `icalendar-date-time'. VTIMEZONE | ||
| 1986 | should be the `icalendar-vtimezone' component in which to interpret DT. | ||
| 1987 | |||
| 1988 | The observance in effect on DT within VTIMEZONE is computed. The | ||
| 1989 | returned value is the value of the `icalendar-tzname' property of this | ||
| 1990 | observance." | ||
| 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 | |||
| 2030 | TZ defaults to the output of `calendar-current-time-zone'; if specified, | ||
| 2031 | it should be a list of the same form as that function returns. | ||
| 2032 | Depending 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 | |||
| 2039 | TZID, if specified, should be a string to identify this time zone; it | ||
| 2040 | defaults to `icalendar-recur--emacs-local-tzid' plus the name of the | ||
| 2041 | standard observance according to `calendar-current-time-zone'. | ||
| 2042 | |||
| 2043 | START-YEAR, if specified, should be an integer giving the year in which | ||
| 2044 | to 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. | ||
| 42 | COMPONENT 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. | ||
| 47 | COMPONENT 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. | ||
| 52 | COMPONENT 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. | ||
| 57 | COMPONENT 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. | ||
| 62 | COMPONENT 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. | ||
| 67 | COMPONENT 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. | ||
| 72 | COMPONENT 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. | ||
| 77 | COMPONENT 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 | |||
| 115 | The 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 | |||
| 119 | If these arguments are all unspecified, the hour, minute, and second | ||
| 120 | slots of the returned date-time will be zero, and it will contain no | ||
| 121 | time zone information. See `icalendar-make-date-time' for more on these | ||
| 122 | arguments." | ||
| 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. | ||
| 134 | An `icalendar-date' value is returned unchanged. | ||
| 135 | An `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. | ||
| 147 | DT 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. | ||
| 154 | DT 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. | ||
| 161 | DT 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. | ||
| 168 | DT may be either an `icalendar-date' or an `icalendar-date-time'. | ||
| 169 | WEEKSTART defaults to 1; it represents the day which starts the week, | ||
| 170 | and 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. | ||
| 183 | DT 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. | ||
| 195 | DT 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. | ||
| 201 | DT 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. | ||
| 207 | DT 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. | ||
| 213 | DT 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. | ||
| 220 | DT1 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. | ||
| 226 | DT1 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 | |||
| 232 | Unlike `icalendar-date-time<', this function assumes both times are | ||
| 233 | local to some time zone and does not consider their zone information. | ||
| 234 | |||
| 235 | If OR-EQUAL is non-nil, this function acts like `<=' rather than `<': | ||
| 236 | it 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 | |||
| 266 | Unlike `icalendar-date-time<', this function assumes both times are | ||
| 267 | local 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 | |||
| 273 | Unlike `icalendar-date-time<=', this function assumes both times are | ||
| 274 | local 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 | |||
| 280 | DT1 and DT2 must both be decoded times, and either both or neither | ||
| 281 | should have time zone information. | ||
| 282 | |||
| 283 | If one has a time zone offset and the other does not, the offset | ||
| 284 | returned from `current-time-zone' is used as the missing offset; if | ||
| 285 | `current-time-zone' cannot provide this information, an error is | ||
| 286 | signaled." | ||
| 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 | |||
| 325 | Note that this function returns nil if DT1 and DT2 represent times in | ||
| 326 | different 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. | ||
| 332 | Note that this function ignores zone information in dt1 and dt2. It | ||
| 333 | returns non-nil if DT1 and DT2 represent the same clock time in | ||
| 334 | different 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 | |||
| 345 | This function returns non-nil if DT1 and DT2 encode to the same Lisp | ||
| 346 | timestamp. Thus they can count as simultaneous even if they represent | ||
| 347 | times in different timezones. If both date-times lack an offset from | ||
| 348 | UTC, they are treated as simultaneous if they encode to the same | ||
| 349 | timestamp in UTC. | ||
| 350 | |||
| 351 | If only one date-time has an offset, they are treated as | ||
| 352 | non-simultaneous if they represent different clock times according to | ||
| 353 | `icalendar-date-time-locally-simultaneous-p'. Otherwise an error is | ||
| 354 | signaled." | ||
| 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. | ||
| 373 | DT1 and DT2 must both be decoded times, and either both or neither must have | ||
| 374 | time 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. | ||
| 380 | DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time' | ||
| 381 | values. 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. | ||
| 396 | DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time' | ||
| 397 | values. 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 | |||
| 413 | The DTS may be any `icalendar-date' or `icalendar-date-time' values, and | ||
| 414 | will 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 | |||
| 420 | The DTS may be any `icalendar-date' or `icalendar-date-time' values, and | ||
| 421 | will 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 | |||
| 427 | UNIT should be `:year', `:month', `:week', or `:day'; time units will be | ||
| 428 | ignored. 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 | |||
| 443 | DT and DELTA should be `icalendar-date-time' values (decoded times), as | ||
| 444 | in `decoded-time-add'. VTIMEZONE, if given, should be an | ||
| 445 | `icalendar-vtimezone'. The resulting date-time will be given the offset | ||
| 446 | determined by VTIMEZONE at the local time determined by adding DELTA to | ||
| 447 | DT. | ||
| 448 | |||
| 449 | This function assumes that time units in DELTA larger than an hour | ||
| 450 | should not affect the local clock time in the result, even when crossing | ||
| 451 | an observance boundary in VTIMEZONE. This means that e.g. if DT is at | ||
| 452 | 9AM daylight savings time on the day before the transition to standard | ||
| 453 | time, then the result of adding a DELTA of two days will be at 9AM | ||
| 454 | standard time, even though this is not exactly 48 hours later. Adding a | ||
| 455 | DELTA of 48 hours, on the other hand, will result in a time exactly 48 | ||
| 456 | hours 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 | |||
| 489 | DT should be an `icalendar-date' or `icalendar-date-time'. UNIT should | ||
| 490 | be `:year', `:month', `:week', `:day', `:hour', `:minute', or `:second'; | ||
| 491 | time units will be ignored if DT is an `icalendar-date'. N may be a | ||
| 492 | positive 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 | |||
| 503 | START should be an `icalendar-date' or `icalendar-date-time'; the | ||
| 504 | returned 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 | |||
| 520 | START should be an `icalendar-date' or `icalendar-date-time'; END must | ||
| 521 | be 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. | ||
| 543 | If DT is an `icalendar-date-time', encode and re-decode it into Emacs | ||
| 544 | local 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 | |||
| 554 | START and END may be either `icalendar-date' or `icalendar-date-time' | ||
| 555 | values. START is an inclusive lower bound, and END is an exclusive | ||
| 556 | upper bound. (Note, however, that if END is a date-time and its time is | ||
| 557 | after midnight, then its date will be included in the returned list.) | ||
| 558 | |||
| 559 | If LOCALLY is non-nil and START and END are date-times, these will be | ||
| 560 | interpreted into Emacs local time, so that the dates returned are valid | ||
| 561 | for 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 | |||
| 593 | This function is like `make-decoded-time', except that it automatically | ||
| 594 | sets the weekday slot set based on the date arguments, and it accepts an | ||
| 595 | additional keyword argument: `:tz'. If provided, its value should be an | ||
| 596 | `icalendar-vtimezone', and the `:zone' and `:dst' arguments should not | ||
| 597 | be provided. In this case, the zone and dst slots in the returned | ||
| 598 | date-time will be adjusted to the correct values in the given time zone | ||
| 599 | for 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 | |||
| 618 | DT should be an `icalendar-date-time'; the keyword arguments have the | ||
| 619 | same meanings as in `make-decoded-time'. The returned variant will have | ||
| 620 | slot values as specified by the arguments or copied from DT, except that | ||
| 621 | the weekday slot will be updated if necessary, and the zone and dst | ||
| 622 | fields will not be set unless given explicitly (because varying the date | ||
| 623 | and clock time generally invalidates the time zone information in DT). | ||
| 624 | |||
| 625 | One additional keyword argument is accepted: `:tz'. If provided, its | ||
| 626 | value should be an `icalendar-vtimezone', an `icalendar-utc-offset', or | ||
| 627 | the symbol \\='preserve. If it is a time zone component, the zone and | ||
| 628 | dst slots in the returned variant will be adjusted to the correct | ||
| 629 | values in the given time zone for the local time represented by the | ||
| 630 | variant. If it is a UTC offset, the variant's zone slot will contain | ||
| 631 | this 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 | ||
| 633 | the 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 | |||
| 671 | DT can be an `icalendar-date' or `icalendar-date-time' value. PERIOD | ||
| 672 | should be an `icalendar-period' value. VTIMEZONE, if given, is passed | ||
| 673 | to `icalendar-period-end' to compute the end time of the period if it | ||
| 674 | was 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 | |||
| 687 | RFC5545 defines week 1 as the first week to include at least four days | ||
| 688 | in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART | ||
| 689 | is specified, in which case it should be an integer between 0 (= Sunday) | ||
| 690 | and 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 | |||
| 704 | RFC5545 defines week 1 as the first week to include at least four days | ||
| 705 | in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART | ||
| 706 | is specified, in which case it should be an integer between 0 (= Sunday) | ||
| 707 | and 6 (= Saturday). The returned value is an `icalendar-date'. | ||
| 708 | |||
| 709 | If WEEKNO is negative, it refers to the WEEKNOth week before the end of | ||
| 710 | the 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 | |||
| 725 | If MONTH is specified, it refers to MONTH in YEAR, and N acts as an | ||
| 726 | index for WEEKDAYs within the month. Otherwise, N acts as an index for | ||
| 727 | WEEKDAYs within the entire YEAR. | ||
| 728 | |||
| 729 | N should be an integer. If N<0, it counts from the end of the month or | ||
| 730 | year: if N=-1, it refers to the last WEEKDAY in the month or year, if | ||
| 731 | N=-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 | ||
| 120 | formatting 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. |
| 144 | This applies only if the summary is not empty! `%s' is replaced | 126 | This applies only if the summary is not empty! `%s' is replaced |
| 145 | by the summary." | 127 | by 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 | ||
| 133 | formatting 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. |
| 152 | This applies only if the description is not empty! `%s' is | 139 | This applies only if the description is not empty! `%s' is |
| 153 | replaced by the description." | 140 | replaced 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 | ||
| 146 | formatting 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. |
| 160 | This applies only if the location is not empty! `%s' is replaced | 152 | This applies only if the location is not empty! `%s' is replaced |
| 161 | by the location." | 153 | by 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 | ||
| 159 | formatting 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. |
| 168 | This applies only if the organizer is not empty! `%s' is | 165 | This applies only if the organizer is not empty! `%s' is |
| 169 | replaced by the organizer." | 166 | replaced 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 | ||
| 172 | formatting 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. |
| 176 | This applies only if the URL is not empty! `%s' is replaced by | 178 | This applies only if the URL is not empty! `%s' is replaced by |
| 177 | the URL." | 179 | the 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 | ||
| 185 | formatting 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." | |||
| 184 | This applies only if the UID is not empty! `%s' is replaced by | 191 | This applies only if the UID is not empty! `%s' is replaced by |
| 185 | the UID." | 192 | the 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 | ||
| 199 | formatting 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. |
| 193 | This applies only if the status is not empty! `%s' is replaced by | 205 | This applies only if the status is not empty! `%s' is replaced by |
| 194 | the status." | 206 | the 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 | ||
| 212 | formatting 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. |
| 201 | This applies only if the class is not empty! `%s' is replaced by | 218 | This applies only if the class is not empty! `%s' is replaced by |
| 202 | the class." | 219 | the 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. | 225 | formatting instead." |
| 209 | Some calendar browsers only propagate recurring events for | 226 | "31.1") |
| 210 | several years beyond the start time. Set this string to a year | 227 | |
| 211 | just 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 |
| 218 | If non-nil hidden diary entries (starting with `&') get exported, | 235 | 'diary-icalendar-export-nonmarking-entries |
| 219 | if 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. | 242 | This string is used by `icalendar-make-uid' to generate UID values when |
| 243 | creating iCalendar components. | ||
| 244 | |||
| 226 | The following specifiers are available: | 245 | The 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) | |
| 235 | For example, a value of \"%s_%h@mydomain.com\" will generate a | 253 | |
| 236 | UID code for each entry composed of the time of the event, a hash | 254 | For example, a value of \"%h%t@mydomain.com\" will generate a UID code |
| 237 | code for the event, and your personal domain name." | 255 | for each entry composed of a hash of the event data, a creation |
| 238 | :type 'string | 256 | timestamp, 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 |
| 244 | In general sexp entries cannot be translated to icalendar format. | 262 | produced by this Emacs." |
| 245 | They are therefore enumerated, i.e. explicitly evaluated for a | 263 | :type 'string) |
| 246 | certain number of days, and then exported. The enumeration starts | 264 | |
| 247 | on 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 | |
| 249 | See `icalendar-export-sexp-enumerate-all' for a list of sexp | 267 | `icalendar-version' property. \"2.0\" is the version corresponding to |
| 250 | entries which by default are NOT enumerated." | 268 | RFC5545.") |
| 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 |
| 258 | If 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 |
| 260 | translating into an icalendar equivalent. This affects the | 278 | "31.1") |
| 261 | following sexp diary entries: `diary-anniversary', | ||
| 262 | `diary-cyclic', `diary-date', `diary-float', `diary-block'. All | ||
| 263 | other 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. | ||
| 306 | A value of 2 only logs errors. | ||
| 307 | A value of 1 also logs warnings. | ||
| 308 | A 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. | ||
| 358 | The 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 | ||
| 363 | buffer.") | ||
| 364 | |||
| 365 | (defun ical:init-error-buffer (&optional err-buffer) | ||
| 366 | "Prepare ERR-BUFFER for iCalendar errors. | ||
| 367 | ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'. | ||
| 368 | Erases 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. | ||
| 378 | ERR-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 | |||
| 400 | Group 1 contains the buffer name where the error originated. | ||
| 401 | Group 2 contains the buffer position. | ||
| 402 | Groups 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 | |||
| 415 | MESSAGE should be a string; it defaults to \"Unknown error\". | ||
| 416 | BUFFER should be a buffer; POSITION should be a position in BUFFER. | ||
| 417 | SEVERITY can be 0 for debug information, or 1 for a warning; otherwise | ||
| 418 | a genuine error is reported. | ||
| 419 | |||
| 420 | The returned error message looks like | ||
| 421 | |||
| 422 | (LEVEL)BUFFER:POSITION: MESSAGE | ||
| 423 | DEBUG-INFO... | ||
| 424 | |||
| 425 | where LEVEL is derived from SEVERITY. DEBUG-INFO contains any additional | ||
| 426 | data in ERROR-PLIST, if `icalendar-debug-level' is | ||
| 427 | 0. `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). | ||
| 452 | ERR-DATA should be a list (ERROR-SYMBOL . SIGNAL-DATA) where | ||
| 453 | SIGNAL-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. | ||
| 473 | If 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'; | ||
| 509 | see `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 | |||
| 524 | CONTENTS can be any object which represents the contents of the | ||
| 525 | iCalendar component for which the UID is generated. If CONTENTS is a | ||
| 526 | string with the text property \\='uid, that property's value will be | ||
| 527 | used as the returned UID. | ||
| 528 | |||
| 529 | Otherwise, CONTENTS will be used to create the hash substituted for | ||
| 530 | \\='%h' in `icalendar-uid-format'. If CONTENTS is not given, the hash | ||
| 531 | will be based on an internal counter, the system name, and the current | ||
| 532 | time in nanoseconds. | ||
| 533 | |||
| 534 | The 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 | |||
| 321 | created buffer all occurrences of CR LF BLANK are replaced by the | 649 | created buffer all occurrences of CR LF BLANK are replaced by the |
| 322 | empty string. Argument FOLDED-ICAL-BUFFER is the folded input | 650 | empty string. Argument FOLDED-ICAL-BUFFER is the folded input |
| 323 | buffer." | 651 | buffer." |
| 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." | |||
| 337 | All occurrences of (CR LF) and (LF CF) are replaced with LF in | 666 | All occurrences of (CR LF) and (LF CF) are replaced with LF in |
| 338 | the current buffer. This is necessary in buffers which contain a | 667 | the current buffer. This is necessary in buffers which contain a |
| 339 | mix of different line endings." | 668 | mix 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..... | |||
| 352 | This function calls itself recursively for each nested calendar element | 682 | This function calls itself recursively for each nested calendar element |
| 353 | it finds. The current buffer should be an unfolded buffer as returned | 683 | it finds. The current buffer should be an unfolded buffer as returned |
| 354 | from `icalendar--get-unfolded-buffer'." | 684 | from `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. |
| 457 | For instance the VCALENDAR node can have VEVENT children as well as VTODO | 792 | For instance the VCALENDAR node can have VEVENT children as well as VTODO |
| 458 | children." | 793 | children." |
| 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." | |||
| 509 | ALIST is an alist entry from a VTIMEZONE, like STANDARD. | 847 | ALIST is an alist entry from a VTIMEZONE, like STANDARD. |
| 510 | DST-P is non-nil if this is for daylight savings time. | 848 | DST-P is non-nil if this is for daylight savings time. |
| 511 | The strings are suitable for assembling into a TZ variable." | 849 | The 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). |
| 562 | Consider only the most recent date specification. | 901 | Consider only the most recent date specification. |
| 563 | Return nil if timezone cannot be parsed." | 902 | Return 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. |
| 579 | ALIST is a VTIMEZONE potentially containing historical records." | 919 | ALIST 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. |
| 601 | Each element of the alist is a cons (ID . TZ-STRING), | 942 | Each element of the alist is a cons (ID . TZ-STRING), |
| 602 | like `icalendar--parse-vtimezone'." | 943 | like `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. |
| 612 | ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'." | 954 | ZONE-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'. | |||
| 628 | RESULT-ZONE, if provided, is the timezone for encoding the result | 971 | RESULT-ZONE, if provided, is the timezone for encoding the result |
| 629 | in any format understood by `decode-time'. | 972 | in any format understood by `decode-time'. |
| 630 | FIXME: multiple comma-separated values should be allowed!" | 973 | FIXME: 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 | ||
| 686 | FIXME: TZID-attributes are ignored....! | 1030 | FIXME: TZID-attributes are ignored....! |
| 687 | FIXME: multiple comma-separated values should be allowed!" | 1031 | FIXME: 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. |
| 741 | Both times must be given in decoded form. One of these times must be | 1086 | Both times must be given in decoded form. One of these times must be |
| 742 | valid (year > 1900 or something)." | 1087 | valid (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)." | |||
| 761 | Optional argument SEPARATOR gives the separator between month, | 1107 | Optional argument SEPARATOR gives the separator between month, |
| 762 | day, and year. If nil a blank character is used as separator. | 1108 | day, and year. If nil a blank character is used as separator. |
| 763 | American format: \"month day year\"." | 1109 | American 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, | |||
| 776 | day, and year. If nil a blank character is used as separator. | 1124 | day, and year. If nil a blank character is used as separator. |
| 777 | European format: (day month year). | 1125 | European format: (day month year). |
| 778 | FIXME" | 1126 | FIXME" |
| 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" | |||
| 790 | Optional argument SEPARATOR gives the separator between month, | 1139 | Optional argument SEPARATOR gives the separator between month, |
| 791 | day, and year. If nil a blank character is used as separator. | 1140 | day, and year. If nil a blank character is used as separator. |
| 792 | ISO format: (year month day)." | 1141 | ISO 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, | |||
| 805 | day, and year. If nil a blank character is used as separator. | 1155 | day, and year. If nil a blank character is used as separator. |
| 806 | Call icalendar--datetime-to-*-date according to the current | 1156 | Call icalendar--datetime-to-*-date according to the current |
| 807 | calendar date style." | 1157 | calendar 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. |
| 814 | Note that this silently ignores seconds." | 1165 | Note 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. |
| 878 | DATE must be a list of the form (month day year). | 1234 | DATE must be a list of the form (month day year). |
| 879 | If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days." | 1235 | If 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 | |||
| 891 | must be either nil or an integer. This function tries to figure | 1248 | must be either nil or an integer. This function tries to figure |
| 892 | the date style from DATESTRING itself. If that is not possible | 1249 | the date style from DATESTRING itself. If that is not possible |
| 893 | it uses the current calendar date style." | 1250 | it 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 | |||
| 981 | AMPMSTRING would be \"pm\". The minutes may be missing as long | 1339 | AMPMSTRING would be \"pm\". The minutes may be missing as long |
| 982 | as the colon is missing as well, i.e. \"9\" is allowed as | 1340 | as the colon is missing as well, i.e. \"9\" is allowed as |
| 983 | TIMESTRING and has the same result as \"9:00\"." | 1341 | TIMESTRING 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. |
| 1019 | All diary entries in the file DIARY-FILENAME are converted to iCalendar | 1378 | All diary entries in the file DIARY-FILENAME are converted to iCalendar |
| 1020 | format. The result is appended to the file ICAL-FILENAME." | 1379 | format. 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\ |
| 1022 | Finto iCalendar file: ") | 1382 | Finto 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. |
| 1032 | ENTRY-FULL is the full diary entry string. CONTENTS is the | 1389 | ENTRY-FULL is the full diary entry string. CONTENTS is the |
| 1033 | current iCalendar object, as a string. Increase | 1390 | current 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. | |||
| 1068 | This function attempts to return t if something goes wrong. In this | 1425 | This function attempts to return t if something goes wrong. In this |
| 1069 | case an error string which describes all the errors and problems is | 1426 | case an error string which describes all the errors and problems is |
| 1070 | written into the buffer `*icalendar-errors*'." | 1427 | written into the buffer `*icalendar-errors*'." |
| 1428 | (declare (obsolete diary-icalendar-export-region "31.1")) | ||
| 1071 | (interactive "r | 1429 | (interactive "r |
| 1072 | FExport diary data into iCalendar file: ") | 1430 | FExport 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. |
| 1180 | NONMARKER is a regular expression matching the start of non-marking | 1538 | NONMARKER is a regular expression matching the start of non-marking |
| 1181 | entries. ENTRY-MAIN is the first line of the diary entry." | 1539 | entries. 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. |
| 1210 | Returns an alist." | 1569 | Returns 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 |
| 1338 | SUMMARY is a string which contains a short description for the | 1699 | SUMMARY is a string which contains a short description for the |
| 1339 | alarm." | 1700 | alarm." |
| 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. |
| 1363 | NONMARKER is a regular expression matching the start of non-marking | 1725 | NONMARKER is a regular expression matching the start of non-marking |
| 1364 | entries. ENTRY-MAIN is the first line of the diary entry." | 1726 | entries. 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. |
| 1447 | Returns day number." | 1810 | Returns 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. |
| 1460 | NONMARKER is a regular expression matching the start of non-marking | 1824 | NONMARKER is a regular expression matching the start of non-marking |
| 1461 | entries. ENTRY-MAIN is the first line of the diary entry." | 1825 | entries. 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. |
| 1542 | NONMARKER is a regular expression matching the start of non-marking | 1907 | NONMARKER is a regular expression matching the start of non-marking |
| 1543 | entries. ENTRY-MAIN is the first line of the diary entry." | 1908 | entries. 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 | ||
| 1627 | Optional argument START determines the first day of the | 1993 | Optional argument START determines the first day of the |
| 1628 | enumeration, given as a Lisp time value -- used for test purposes." | 1994 | enumeration, 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. |
| 1679 | NONMARKER is a regular expression matching the start of non-marking | 2046 | NONMARKER is a regular expression matching the start of non-marking |
| 1680 | entries. ENTRY-MAIN is the first line of the diary entry." | 2047 | entries. 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 | ||
| 1818 | NONMARKER is a regular expression matching the start of non-marking | 2187 | NONMARKER is a regular expression matching the start of non-marking |
| 1819 | entries. ENTRY-MAIN is the first line of the diary entry." | 2188 | entries. 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. |
| 1831 | NONMARKER is a regular expression matching the start of non-marking | 2201 | NONMARKER is a regular expression matching the start of non-marking |
| 1832 | entries. ENTRY-MAIN is the first line of the diary entry." | 2202 | entries. 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. |
| 1905 | NONMARKER is a regular expression matching the start of non-marking | 2276 | NONMARKER is a regular expression matching the start of non-marking |
| 1906 | entries. ENTRY-MAIN is the first line of the diary entry." | 2277 | entries. 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. | |||
| 1986 | Argument DIARY-FILENAME input `diary-file'. | 2358 | Argument DIARY-FILENAME input `diary-file'. |
| 1987 | Optional argument NON-MARKING determines whether events are created as | 2359 | Optional argument NON-MARKING determines whether events are created as |
| 1988 | non-marking or not." | 2360 | non-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. | |||
| 2012 | Return code t means that importing worked well, return code nil | 2385 | Return code t means that importing worked well, return code nil |
| 2013 | means that an error has occurred. Error messages will be in the | 2386 | means that an error has occurred. Error messages will be in the |
| 2014 | buffer `*icalendar-errors*'." | 2387 | buffer `*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. | |||
| 2093 | This function attempts to return t if something goes wrong. In this | 2468 | This function attempts to return t if something goes wrong. In this |
| 2094 | case an error string which describes all the errors and problems is | 2469 | case an error string which describes all the errors and problems is |
| 2095 | written into the buffer `*icalendar-errors*'." | 2470 | written 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*'." | |||
| 2255 | DTSTART-DEC is the DTSTART property of E. | 2631 | DTSTART-DEC is the DTSTART property of E. |
| 2256 | START-T is the event's start time in diary format. | 2632 | START-T is the event's start time in diary format. |
| 2257 | END-T is the event's end time in diary format." | 2633 | END-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." | |||
| 2492 | DTSTART is the decoded DTSTART property of E. | 2869 | DTSTART is the decoded DTSTART property of E. |
| 2493 | Argument START-D gives the first day. | 2870 | Argument START-D gives the first day. |
| 2494 | Argument END-D gives the last day." | 2871 | Argument 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." | |||
| 2503 | DTSTART-DEC is the decoded DTSTART property of E. | 2881 | DTSTART-DEC is the decoded DTSTART property of E. |
| 2504 | START-T is the event's start time in diary format. | 2882 | START-T is the event's start time in diary format. |
| 2505 | END-T is the event's end time in diary format." | 2883 | END-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 | |||
| 2523 | SUMMARY is not nil it must be a string that gives the summary of the | 2902 | SUMMARY is not nil it must be a string that gives the summary of the |
| 2524 | entry. In this case the user will be asked whether he wants to insert | 2903 | entry. In this case the user will be asked whether he wants to insert |
| 2525 | the entry." | 2904 | the 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." | |||
| 573 | Argument EVENT is the mouse clicked event." | 573 | Argument 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. | |||
| 3715 | You can use `add-hook' to add functions to this list | 3715 | You can use `add-hook' to add functions to this list |
| 3716 | either globally or locally.") | 3716 | either 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'. |
| 1073 | Frames with a non-nil `desktop-dont-save' parameter are not saved." | 1078 | Frames with a non-nil `desktop-dont-save' parameter are not saved. |
| 1079 | Likewise 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'. |
| 957 | Document types are symbols like `dvi', `ps', `pdf', `epub', | 957 | Document 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 | |||
| 381 | of these warnings masse. In almost any other case, setting | 381 | of these warnings masse. In almost any other case, setting |
| 382 | this to anything but t is likely to be counter-productive.") | 382 | this 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. | ||
| 2843 | Report 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 | ||
| 59 | CONDITION can be a Lisp expression, as in `cond'. | 59 | CONDITION can be a Lisp expression, as in `cond'. |
| 60 | Or it can be one of `(bind* BINDINGS...)', `(match* PATTERN DATUM)', | 60 | Or it can be one of `(bind* BINDINGS...)', `(match* PATTERN DATUM)', |
| 61 | or `(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*') |
| 64 | for the body of the clause, and all subsequent clauses, since the `bind*' | 64 | for 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*' | |||
| 81 | or is the last clause, the value of the last expression | 81 | or is the last clause, the value of the last expression |
| 82 | in its body becomes the return value of the `cond*' construct. | 82 | in its body becomes the return value of the `cond*' construct. |
| 83 | 83 | ||
| 84 | Non-exit clause: | 84 | Non-exit clauses: |
| 85 | 85 | ||
| 86 | If a clause has only one element, or if its first element is | 86 | If a clause has only one element, or if its first element is t or a |
| 87 | t 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 |
| 88 | Instead, control always falls through to the next clause (if any). | 88 | clause never exits the `cond*' construct. Instead, control always falls |
| 89 | All bindings made in CONDITION for the BODY of the non-exit clause | 89 | through to the next clause (if any). Except for a `bind-and*' clause, |
| 90 | are passed along to the rest of the clauses in this `cond*' construct. | 90 | all bindings made in CONDITION for the BODY of the non-exit clause are |
| 91 | passed along to the rest of the clauses in this `cond*' construct. | ||
| 91 | 92 | ||
| 92 | \\[match*] for documentation of the patterns for use in `match*'." | 93 | See `match*' for documentation of the patterns for use in `match*' |
| 94 | conditions." | ||
| 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. | ||
| 548 | You can set this value to `mode-line' (default) to indicate the | ||
| 549 | availability of a package suggestion in the minor mode, `always' to | ||
| 550 | prompt the user in the minibuffer every time a suggestion is available | ||
| 551 | in a `fundamental-mode' buffer, or `message' to just display a message | ||
| 552 | hinting at the existence of a suggestion. If you only wish to be | ||
| 553 | reminded of package suggestions once every session, consider customizing | ||
| 554 | the `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. | ||
| 567 | Each entry in the list is of a form suitable to for | ||
| 568 | `package--suggestion-applies-p', which see. The special value `unset' | ||
| 569 | is used to indicate that `package--autosuggest-find-candidates' should | ||
| 570 | load the database into memory.") | ||
| 571 | |||
| 572 | (defvar package--autosuggest-suggested '() | ||
| 573 | "List of packages that have already been suggested. | ||
| 574 | Suggestions 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 | ||
| 576 | here will inhibit `package-autosuggest-mode' from displaying a hint in | ||
| 577 | the mode line).") | ||
| 578 | |||
| 579 | (defun package--suggestion-applies-p (sug) | ||
| 580 | "Check if a suggestion SUG is applicable to the current buffer. | ||
| 581 | Each suggestion has the form (PACKAGE TYPE DATA), where PACKAGE is a | ||
| 582 | symbol denoting the package and major-mode the suggestion applies to, | ||
| 583 | TYPE is one of `auto-mode-alist', `magic-mode-alist' or | ||
| 584 | `interpreter-mode-alist' indicating the type of check to be made and | ||
| 585 | DATA 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 | ||
| 587 | PACKAGE should be suggested for). If the package name and the major | ||
| 588 | mode name differ, then an optional forth element MAJOR-MODE can indicate | ||
| 589 | what 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. | ||
| 614 | The 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. | ||
| 652 | This 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. | ||
| 4537 | SUG 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. | ||
| 4553 | PACKAGES is a list of package suggestions in the form as described in | ||
| 4554 | `package--suggestion-applies-p'. The function returns a non-nil value | ||
| 4555 | if 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. | ||
| 4567 | The package manager can provide the editor support for these kinds of | ||
| 4568 | files 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, | ||
| 4572 | so 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. | ||
| 4630 | The optional argument CANDIDATES may be a list of packages that match | ||
| 4631 | for form described in `package--suggestion-applies-p'. If omitted, the | ||
| 4632 | list 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. | ||
| 4641 | Emacs will remember if you have previously rejected a suggestion during | ||
| 4642 | a session and won't mention it afterwards. If you have made a mistake | ||
| 4643 | or would like to reconsider this, use this command to want to reset the | ||
| 4644 | suggestions." | ||
| 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 |
| 572 | defaults to `equal'. | 572 | defaults to `equal'. |
| 573 | This does not modify SEQUENCE1 or SEQUENCE2." | 573 | This 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 |
| 584 | defaults to `equal'. | 584 | defaults to `equal'. |
| 585 | This does not modify SEQUENCE1 or SEQUENCE2." | 585 | This 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. | ||
| 363 | Call this with a work buffer as the current buffer. | ||
| 364 | BUFFER is the originating buffer and if non-nil, make the current | ||
| 365 | buffer'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. | ||
| 405 | If BUFFER is non-nil, use the face remappings, alternative and default | ||
| 406 | properties from that buffer when determining the width. | ||
| 407 | If you call this function to measure pixel width of a string | ||
| 408 | with embedded newlines, it returns the width of the widest | ||
| 409 | substring that does not include newlines. | ||
| 410 | |||
| 411 | If ELLIPSIS is non-nil, it should be a string which will replace the end | ||
| 412 | of STRING if it extends beyond MAX-PIXELS, unless the pixel width of | ||
| 413 | STRING is equal to or less than the pixel width of ELLIPSIS. If it is | ||
| 414 | non-nil and not a string, then ELLIPSIS defaults to | ||
| 415 | `truncate-string-ellipsis', or to three dots when it's nil. | ||
| 416 | |||
| 417 | If ELLIPSIS-PIXELS is non-nil, it is the pixel width of ELLIPSIS, and | ||
| 418 | can be used to avoid the cost of recomputing this for multiple calls to | ||
| 419 | this 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. |
| 397 | This takes into account combining characters and grapheme clusters: | 465 | This 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'. |
| 43 | Those functions will be run after the header line and argument | 43 | Those functions will be run after the header line, the argument |
| 44 | list was inserted, and before the documentation is inserted. | 44 | list, and the function's documentation are inserted. |
| 45 | The functions will be called with one argument: the function's symbol. | 45 | The functions will be called with one argument: the function's symbol. |
| 46 | They can assume that a newline was output just before they were called, | 46 | They can assume that a newline was output just before they were called, |
| 47 | and they should terminate any of their own output with a newline. | 47 | and 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 | |||
| 501 | calling command was invoked interactively. In this case the stack of | 501 | calling command was invoked interactively. In this case the stack of |
| 502 | items for help buffer \"back\" buttons is cleared. | 502 | items for help buffer \"back\" buttons is cleared. |
| 503 | 503 | ||
| 504 | This should be called very early, before the output buffer is cleared, | 504 | This function also re-enables the major mode of the buffer, thus |
| 505 | because we want to record the \"previous\" position of point so we can | 505 | resetting local variables to the values set by the mode and running the |
| 506 | restore it properly when going back." | 506 | mode hooks. |
| 507 | |||
| 508 | So this should be called very early, before the output buffer is | ||
| 509 | cleared, also because we want to record the \"previous\" position of | ||
| 510 | point 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. |
| 81 | This means to show completions even when the current minibuffer contents | 81 | This means to show completions even when the current minibuffer contents |
| 82 | is the same as was the initial input after minibuffer activation. | 82 | is the same as the initial input after minibuffer activation. |
| 83 | This also means that if you traverse the list of completions with | 83 | This also means that if you just hit \\`C-j' without typing any |
| 84 | commands like \\`C-.' and just hit \\`RET' without typing any | 84 | characters, this chooses the first completion candidate instead of the |
| 85 | characters, the match under point will be chosen instead of the | 85 | minibuffer's default value. |
| 86 | default." | 86 | |
| 87 | See 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. |
| 251 | If there is a completion candidate and the minibuffer contents is the | ||
| 252 | same as it was right after minibuffer activation, exit selecting that | ||
| 253 | candidate. Otherwise do as `minibuffer-complete-and-exit'. | ||
| 254 | |||
| 255 | You may wish to consider binding this command to \\`RET' (or to | ||
| 256 | `<remap> <minibuffer-complete-and-exit>') in `icomplete-minibuffer-map'. | ||
| 257 | If you do that, then when Emacs first prompts for input such that the | ||
| 258 | current minibuffer contents is equal to the initial input right after | ||
| 259 | minibuffer activation, \\`RET' chooses the first completion candidate | ||
| 260 | instead of the minibuffer's default value. | ||
| 261 | This 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.") | |||
| 274 | Default value, nil, means edit the string instead." | 274 | Default 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. |
| 281 | Value is nil, t, or a function. | 279 | Value 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. |
| 2833 | COUNT (interactively, the prefix argument) defaults to 1. | 2830 | COUNT (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. |
| 610 | Sort ALIST first if `json-encoding-object-sort-predicate' is | 610 | Sort ALIST first if `json-encoding-object-sort-predicate' is |
| 611 | non-nil. Sorting can optionally be DESTRUCTIVE for speed." | 611 | non-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. | ||
| 817 | Entries 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. | ||
| 867 | Entries 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. | ||
| 878 | Entries 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. | ||
| 884 | Entries 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 | |||
| 890 | Modifiers: | ||
| 891 | |||
| 892 | | Key | Translation | Description | | ||
| 893 | |-----+-------------+--------------------------| | ||
| 894 | | :: | · | Vowel length | | ||
| 895 | |||
| 896 | Stress diacritics: | ||
| 897 | |||
| 898 | | Key | Description | Example | | ||
| 899 | |------+--------------+---------| | ||
| 900 | | \\=' | Acute accent | a' -> á | | ||
| 901 | | \\=` | Grave accent | a` -> à | | ||
| 902 | |||
| 903 | Doubling the postfix separates the letter and the postfix. | ||
| 904 | |||
| 905 | Vowels: | ||
| 906 | |||
| 907 | | Key | Translation | Description | | ||
| 908 | |-----+-------------+---------------------------------| | ||
| 909 | | e, | ę | Mid front nasal vowel | | ||
| 910 | | E, | Ę | Mid front nasal vowel (capital) | | ||
| 911 | |||
| 912 | a, e, i, and u are bound to a single key. | ||
| 913 | |||
| 914 | Consonants: | ||
| 915 | |||
| 916 | | Key | Translation | Description | | ||
| 917 | |-------+-------------+------------------------------------| | ||
| 918 | | ;; | ˀ | Glottal stop | | ||
| 919 | | c/ | č | Postalveolar affricate | | ||
| 920 | | C/ | Č | Postalveolar affricate (capital) | | ||
| 921 | | t/ | θ | Voiceless dental fricative | | ||
| 922 | |||
| 923 | h, k, n, r, s, t, w, and y are bound to a single key. | ||
| 924 | |||
| 925 | b, l, m, and p are used rarely in loanwords. They are also each bound | ||
| 926 | to a single key. | ||
| 927 | |||
| 928 | Stress exception markers: | ||
| 929 | |||
| 930 | | Key | Description | Example | | ||
| 931 | |-----+--------------------+----------| | ||
| 932 | | _ | Combining low line | a_ -> a̲ | | ||
| 933 | |||
| 934 | Note: Not all fonts can properly display a combining low line on all | ||
| 935 | letters. | ||
| 936 | |||
| 937 | Underlining has been used by some to indicate that vowels behave | ||
| 938 | exceptionally with regard to stress placement. Alternatively, markup or | ||
| 939 | other methods can be used to create an underlining effect. | ||
| 940 | |||
| 941 | To enter a plain underscore, type the underscore twice. | ||
| 942 | |||
| 943 | All Haudenosaunee languages, including Tuscarora can be input | ||
| 944 | simultaneously 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. |
| 862 | Entries are as with rules in `quail-define-rules'.") | 1016 | Entries 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. |
| 871 | Entries are as with rules in `quail-define-rules'.") | 1026 | Entries 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. |
| 886 | Entries are as with rules in `quail-define-rules'.") | 1042 | Entries 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. |
| 892 | Entries are as with rules in `quail-define-rules'.") | 1048 | Entries 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 | ||
| 910 | Modifiers: | 1067 | Modifiers: |
| 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 | ||
| 993 | Consonants: | 1156 | Consonants: |
| 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 | ||
| 1027 | Devoicing: | 1198 | Phonological 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 | |||
| 1035 | letters and a combining macron below on all vowels. | 1206 | letters and a combining macron below on all vowels. |
| 1036 | 1207 | ||
| 1037 | Underlining is commonly used in Oneida to indicate devoiced syllables on | 1208 | Underlining is commonly used in Oneida to indicate devoiced syllables on |
| 1038 | pre-pausal forms (also called utterance-final forms). Alternatively, | 1209 | pre-pausal forms (also called utterance-final forms), and it has been |
| 1039 | markup or other methods can be used to create an underlining effect. | 1210 | used in some Tuscarora orthographies to indicate that vowels behave |
| 1211 | exceptionally with regard to stress placement. Alternatively, markup or | ||
| 1212 | other methods can be used to create an underlining effect. | ||
| 1040 | 1213 | ||
| 1041 | To enter a plain underscore, the underscore twice. | 1214 | To 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 | ||
| 1047 | There are individual input methods for each of the languages that can be | 1220 | There are individual input methods for each of the languages that can be |
| 1048 | entered with this input method: `mohawk-postfix', `oneida-postfix', | 1221 | entered 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 | |||
| 319 | is non-nil, the invoked method may interactively prompt the user | 319 | is non-nil, the invoked method may interactively prompt the user |
| 320 | for authorization. The default is nil. | 320 | for authorization. The default is nil. |
| 321 | 321 | ||
| 322 | If the parameter `:keep-fd' is given, and the return message has a first | ||
| 323 | argument with a D-Bus type `:unix-fd', the returned file desriptor is | ||
| 324 | kept internally, and can be used in a later `dbus--close-fd' call. | ||
| 325 | |||
| 322 | All other arguments ARGS are passed to METHOD as arguments. They are | 326 | All other arguments ARGS are passed to METHOD as arguments. They are |
| 323 | converted into D-Bus types via the following rules: | 327 | converted into D-Bus types via the following rules: |
| 324 | 328 | ||
| @@ -453,6 +457,10 @@ If the parameter `:authorizable' is given and the following AUTH | |||
| 453 | is non-nil, the invoked method may interactively prompt the user | 457 | is non-nil, the invoked method may interactively prompt the user |
| 454 | for authorization. The default is nil. | 458 | for authorization. The default is nil. |
| 455 | 459 | ||
| 460 | If the parameter `:keep-fd' is given, and the return message has a first | ||
| 461 | argument with a D-Bus type `:unix-fd', the returned file desriptor is | ||
| 462 | kept internally, and can be used in a later `dbus--close-fd' call. | ||
| 463 | |||
| 456 | All other arguments ARGS are passed to METHOD as arguments. They are | 464 | All other arguments ARGS are passed to METHOD as arguments. They are |
| 457 | converted into D-Bus types via the following rules: | 465 | converted 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. |
| 609 | The return value is a list, with elements of kind (KEY . VALUE). | 618 | The 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. |
| 1061 | FMT and ARGS are passed to `error'." | 1061 | FMT 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." | |||
| 5608 | Similar to `tramp-send-command-and-check' but accepts two more arguments | 5614 | Similar to `tramp-send-command-and-check' but accepts two more arguments |
| 5609 | FMT and ARGS which are passed to `error'." | 5615 | FMT 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." | |||
| 5505 | This is the fallback implementation for backends which do not | 5508 | This is the fallback implementation for backends which do not |
| 5506 | support symbolic links." | 5509 | support 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. |
| 2030 | SUCCESS-FN, ERROR-FN and TIMEOUT-FN run in buffer of call site. | 2038 | SUCCESS-FN, ERROR-FN and TIMEOUT-FN run in buffer of call site. |
| 2031 | HINT argument is a symbol passed as DEFERRED to `jsonrpc-async-request' | 2039 | HINT argument is a symbol passed as DEFERRED to `jsonrpc-async-request' |
| 2032 | and also used as a hint of the request cancellation mechanism (see | 2040 | and 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'. |
| 3508 | If KEEP, knowingly push a dummy do-nothing update." | 3516 | If 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 | ||
| 587 | See `project-vc-extra-root-markers' for the marker value format.") | 594 | See `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. | ||
| 599 | It can be nil, a number, or an alist where | ||
| 600 | the key is a predicate, and the value is a number. | ||
| 601 | A predicate function should take a directory string and if it returns | ||
| 602 | non-nil, the corresponding value will be used as the timeout. | ||
| 603 | Set 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. | ||
| 608 | The format of the value is same as `project-vc-cache-timeout', but while | ||
| 609 | the former is intended for interactive commands, this variable uses | ||
| 610 | higher numbers, intended for \"background\" things like | ||
| 611 | `project-mode-line' indicators and `project-uniquify-dirname-transform'. | ||
| 612 | It 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) | 654 | The value is cached, and depending on whether MAYBE-PROMPT was non-nil |
| 595 | ;; FIXME: Cache for a shorter time (bug#78545). | 655 | in 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 | |||
| 1005 | The value is cached, and depending on whether `non-essential' is nil, | ||
| 1006 | the 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 | |||
| 2206 | from the list using REPORT-MESSAGE, which is a format string | 2287 | from the list using REPORT-MESSAGE, which is a format string |
| 2207 | passed to `message' as its first argument." | 2288 | passed 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. | |||
| 2385 | Display a message at the end summarizing what was found. | 2466 | Display a message at the end summarizing what was found. |
| 2386 | Return the number of detected projects." | 2467 | Return 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 | |||
| 2417 | projects should be deleted." | 2499 | projects 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. | |||
| 2435 | Display a message at the end summarizing what was forgotten. | 2523 | Display a message at the end summarizing what was forgotten. |
| 2436 | Return the number of forgotten projects." | 2524 | Return 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 | |||
| 2624 | the buffer's directory name when buffers from two different projects | 2713 | the buffer's directory name when buffers from two different projects |
| 2625 | would otherwise have the same name." | 2714 | would 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. | ||
| 2639 | Used by `project-name-cached'.") | ||
| 2640 | |||
| 2641 | (defun project-name-cached (dir) | ||
| 2642 | "Return the cached project name for the directory DIR. | ||
| 2643 | Until it's cached, retrieve the project name using `project-current' | ||
| 2644 | and `project-name', then put the name to the cache for the time defined | ||
| 2645 | by the variable `project-name-cache-timeout'. This function is useful | ||
| 2646 | for 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. | ||
| 3371 | Use `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. |
| 3371 | If DEDICATED is nil, this is simply `python-shell-buffer-name'. | 3381 | If 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. |
| 3831 | CMD is the Python command to be executed and PROC-NAME is the | 3828 | CMD 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 | ||
| 257 | The result must be a list of xref objects. If IDENTIFIER | 255 | The 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 | ||
| 267 | To create an xref object, call `xref-make'.") | 265 | To 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. |
| 290 | The second argument has the same meaning as in `apropos'. | 289 | The second argument has the same meaning as in `apropos'. |
| 291 | 290 | ||
| 292 | If BACKEND is implemented in Lisp, it can use | 291 | If 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. |
| 5449 | More specifically if `debug-on-error' is set then the debugger will be invoked | 5449 | More specifically, if `debug-on-error' is set, then the debugger will |
| 5450 | even if this catches the signal." | 5450 | be invoked even if some handler catches the signal. |
| 5451 | Note that this doesn't prevent the handler from executing, it just | ||
| 5452 | causes 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. | ||
| 109 | If 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. | ||
| 120 | Each function is called with one argument EVENT, a sleep event. EVENT | ||
| 121 | state can be retrieved via \\+`(sleep-event-state EVENT)'. It will be | ||
| 122 | one of the symbols \\+`pre-sleep' or \\+`post-wake'. | ||
| 123 | |||
| 124 | Handling \\+`pre-sleep' events should be done as fast as possible, do as | ||
| 125 | little as possible and avoid user prompts. Systems often grant a very | ||
| 126 | short pre-sleep processing interval, typically ranging between 2 and 5 | ||
| 127 | seconds. The system may sleep even if your processing is not complete. | ||
| 128 | For example, your function could close active connections or serial | ||
| 129 | ports. | ||
| 130 | |||
| 131 | Handling \\+`post-wake' events offers more leeway. Your function could | ||
| 132 | reestablish connections. | ||
| 133 | |||
| 134 | Note: Your code, or the functions it calls, should not raise any signals | ||
| 135 | or all hooks will be halted preventing other hook functions from | ||
| 136 | cleaning up or waking up. You can wrap your code in a `condition-case' | ||
| 137 | block." | ||
| 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. | ||
| 144 | Optional WHY is a string that identifies a sleep block to system utility | ||
| 145 | commands that inspect system-wide blocks. WHY defaults to \"Emacs\". | ||
| 146 | |||
| 147 | Optional ALLOW-DISPLAY-SLEEP, when non-nil, allows the display to sleep | ||
| 148 | or a screen saver to run while the system idle sleep is blocked. The | ||
| 149 | default is to keep the display active. | ||
| 150 | |||
| 151 | Return a sleep blocking token. You must retain this value and provide | ||
| 152 | it to `system-sleep-unblock-sleep' to unblock its associated block. | ||
| 153 | |||
| 154 | Return nil if system sleep cannot be inhibited. | ||
| 155 | |||
| 156 | Note: All active blocks are released when the Emacs process dies. | ||
| 157 | Despite this, you should unblock your blocks when your processing is | ||
| 158 | complete. 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. | ||
| 164 | Return non-nil TOKEN was unblocked, or nil if not. | ||
| 165 | In the unlikely event that unblock fails, the block will be released | ||
| 166 | when 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. | ||
| 173 | The optional arguments WHY and ALLOW-DISPLAY-SLEEP have the same meaning | ||
| 174 | as in `system-sleep-block-sleep', which see. | ||
| 175 | The 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. | ||
| 237 | Return 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. | ||
| 244 | WHY is a string that identifies a sleep block to system utility commands | ||
| 245 | that inspect system-wide blocks. | ||
| 246 | When non-nil, ALLOW-DISPLAY-SLEEP allows the display to sleep or a | ||
| 247 | screen saver to run while the system idle sleep is blocked. The default | ||
| 248 | is to keep the display active. | ||
| 249 | Return a sleep-block token.") | ||
| 250 | |||
| 251 | (cl-defgeneric system-sleep--unblock-sleep (token) | ||
| 252 | "Unblock the system sleep block associated with TOKEN. | ||
| 253 | Return 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. | ||
| 279 | The 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. |
| 51 | If non-nil, this should be a single string with command-line options | 51 | This should be a list of strings, each one passed as a separate argument |
| 52 | for the yamllint command, with individual options separated by whitespace." | 52 | to 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. | ||
| 182 | Use the function `customize-variable' to choose a common format, and/or | ||
| 183 | see 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 | ||
| 85 | When CUA mode is enabled, you can use C-z, C-x, C-c, and C-v to | 84 | When CUA mode is enabled, you can use C-z, C-x, C-c, and C-v to |
| 86 | undo, cut, copy, and paste in addition to the normal Emacs | 85 | undo, 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 | ||
| 289 | from the Emacs default:\n\n" ) | 287 | from 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 " |
| 337 | It is OK to change key bindings, but changed bindings do not | 335 | It is OK to change key bindings, but changed bindings do not |
| 338 | correspond to what the tutorial says.\n\n") | 336 | correspond 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. |
| 203 | These bindings are also available with an ESC prefix | 203 | These 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 |
| 205 | and with a `diff-minor-mode-prefix' prefix in `diff-minor-mode'. | 205 | particular read-write `diff-mode' buffers, and with a |
| 206 | `diff-minor-mode-prefix' prefix in `diff-minor-mode'. | ||
| 206 | See also `diff-mode-read-only-map'." | 207 | See 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. |
| 234 | Most of the bindings for read-only `diff-mode' buffers are in | 228 | Most 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 |
| 236 | read-only `diff-mode' buffers that are *not* available with an ESC | 230 | read-only `diff-mode' buffers that are *not* also available with an ESC |
| 237 | prefix (i.e. a \\=`M-' prefix) in read-write `diff-mode' buffers." | 231 | prefix (i.e. a \\=`M-' prefix) in read-write (nor read-only) `diff-mode' |
| 232 | buffers." | ||
| 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) | 894 | When killing the last hunk left for a file, kill the file header too. |
| 888 | (if (not (diff--some-hunks-p)) | 895 | Interactively, if the region is active, kill all hunks that the region |
| 889 | (error "No hunks") | 896 | overlaps. |
| 890 | (diff-beginning-of-hunk t) | 897 | |
| 891 | (let* ((hunk-bounds (diff-bounds-of-hunk)) | 898 | When called from Lisp with optional arguments BEG and END non-nil, kill |
| 892 | (file-bounds (ignore-errors (diff-bounds-of-file))) | 899 | all 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 | 900 | interactively 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. | ||
| 2282 | If BEG and END are nil, kill the hunk at point. | ||
| 2283 | Otherwise kill all hunks overlapped by region delimited by BEG and END. | ||
| 2284 | When killing a hunk that's the only one remaining for its file, kill the | ||
| 2285 | file header too. | ||
| 2286 | If 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. |
| 2287 | Interactively, if the region is active, reverse-apply and kill all | 2359 | Interactively, 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. |
| 389 | Intended to be used as the value of `vc-filter-command-function'." | 392 | Intended 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. | ||
| 777 | If there is a configured upstream, return the remote-tracking branch | ||
| 778 | with key `upstream'. If there is a distinct configured push remote, | ||
| 779 | return the remote-tracking branch there with key `push'. | ||
| 780 | A configured push remote that's just the same as the upstream remote is | ||
| 781 | ignored 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. |
| 777 | This is able to identify topic branches for certain forge workflows." | 804 | This 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. |
| 796 | This works by considering the current branch as a topic branch | 810 | This works by considering the current branch as a topic branch |
| 797 | (whether or not it actually is). | 811 | (whether or not it actually is). |
| 798 | Requires that the corresponding trunk exists as a local branch. | 812 | |
| 799 | 813 | If there is a distinct push remote for this branch, assume the target | |
| 800 | The algorithm employed is as follows. Find all merge bases between the | 814 | for outstanding changes is the tracking branch, and return that. |
| 801 | current branch and other local branches. Each of these is a commit on | 815 | |
| 802 | the current branch. Use `git merge-base --independent' on them all to | 816 | Otherwise, fall back to the following algorithm, which requires that the |
| 803 | find the topologically most recent. Take the branch for which that | 817 | corresponding trunk exists as a local branch. Find all merge bases |
| 804 | commit is a merge base with the current branch to be the branch into | 818 | between the current branch and other local branches. Each of these is a |
| 805 | which the current branch will eventually be merged. Find its upstream. | 819 | commit on the current branch. Use `git merge-base --independent' on |
| 806 | (If there is more than one branch whose merge base with the current | 820 | them all to find the topologically most recent. Take the branch for |
| 807 | branch is that same topologically most recent commit, try them | 821 | which that commit is a merge base with the current branch to be the |
| 808 | one-by-one, accepting the first that has an upstream.)" | 822 | branch into which the current branch will eventually be merged. Find |
| 809 | (cl-flet ((get-line () (buffer-substring (point) (pos-eol)))) | 823 | its upstream. (If there is more than one branch whose merge base with |
| 810 | (let* ((branches (vc-git-branches)) | 824 | the current branch is that same topologically most recent commit, try |
| 811 | (current (pop branches)) | 825 | them 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'.) |
| 3332 | REFRESH is passed on to `vc--incoming-revision'." | 3332 | REFRESH 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 | ||
| 3350 | When unspecified, UPSTREAM-LOCATION is the outgoing base. | 3348 | When unspecified, UPSTREAM-LOCATION is the outgoing base. |
| 3351 | For a trunk branch this is always the place \\[vc-push] would push to. | 3349 | For a trunk branch this is always the place \\[vc-push] would push to. |
| 3352 | For a topic branch, query the backend for an appropriate outgoing base. | 3350 | For a topic branch, see whether the branch matches one of |
| 3351 | `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query | ||
| 3352 | the backend for an appropriate outgoing base. | ||
| 3353 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding | 3353 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding |
| 3354 | the difference between trunk and topic branches. | 3354 | the difference between trunk and topic branches. |
| 3355 | 3355 | ||
| @@ -3377,7 +3377,9 @@ Uncommitted changes are included in the diff. | |||
| 3377 | 3377 | ||
| 3378 | When unspecified, UPSTREAM-LOCATION is the outgoing base. | 3378 | When unspecified, UPSTREAM-LOCATION is the outgoing base. |
| 3379 | For a trunk branch this is always the place \\[vc-push] would push to. | 3379 | For a trunk branch this is always the place \\[vc-push] would push to. |
| 3380 | For a topic branch, query the backend for an appropriate outgoing base. | 3380 | For a topic branch, see whether the branch matches one of |
| 3381 | `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query | ||
| 3382 | the backend for an appropriate outgoing base. | ||
| 3381 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding | 3383 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding |
| 3382 | the difference between trunk and topic branches. | 3384 | the difference between trunk and topic branches. |
| 3383 | 3385 | ||
| @@ -3411,7 +3413,9 @@ working revision and UPSTREAM-LOCATION. | |||
| 3411 | 3413 | ||
| 3412 | When unspecified, UPSTREAM-LOCATION is the outgoing base. | 3414 | When unspecified, UPSTREAM-LOCATION is the outgoing base. |
| 3413 | For a trunk branch this is always the place \\[vc-push] would push to. | 3415 | For a trunk branch this is always the place \\[vc-push] would push to. |
| 3414 | For a topic branch, query the backend for an appropriate outgoing base. | 3416 | For a topic branch, see whether the branch matches one of |
| 3417 | `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query | ||
| 3418 | the backend for an appropriate outgoing base. | ||
| 3415 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding | 3419 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding |
| 3416 | the difference between trunk and topic branches. | 3420 | the difference between trunk and topic branches. |
| 3417 | 3421 | ||
| @@ -3443,7 +3447,9 @@ working revision and UPSTREAM-LOCATION. | |||
| 3443 | 3447 | ||
| 3444 | When unspecified, UPSTREAM-LOCATION is the outgoing base. | 3448 | When unspecified, UPSTREAM-LOCATION is the outgoing base. |
| 3445 | For a trunk branch this is always the place \\[vc-push] would push to. | 3449 | For a trunk branch this is always the place \\[vc-push] would push to. |
| 3446 | For a topic branch, query the backend for an appropriate outgoing base. | 3450 | For a topic branch, see whether the branch matches one of |
| 3451 | `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query | ||
| 3452 | the backend for an appropriate outgoing base. | ||
| 3447 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding | 3453 | See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding |
| 3448 | the difference between trunk and topic branches. | 3454 | the 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 | |||
| 1334 | This is much faster.") | 1334 | This 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. |
| 1338 | ARG may be negative to move backward. | 1338 | ARG may be negative to move backward. |
| 1339 | When the second optional argument is non-nil, | 1339 | If the optional argument SUPPRESS-ECHO is non-nil, suppress showing |
| 1340 | nothing is shown in the echo area." | 1340 | in 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. |
| 1388 | With optional ARG, move across that many fields. | 1388 | Interactively, ARG is the prefix numeric argument and defaults to 1. |
| 1389 | When the second optional argument is non-nil, | 1389 | If the optional argument SUPPRESS-ECHO is non-nil, suppress showing |
| 1390 | nothing is shown in the echo area." | 1390 | in 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. |
| 1397 | With optional ARG, move across that many fields. | 1397 | Interactively, ARG is the prefix numeric argument and defaults to 1. |
| 1398 | When the second optional argument is non-nil, | 1398 | If the optional argument SUPPRESS-ECHO is non-nil, suppress showing |
| 1399 | nothing is shown in the echo area." | 1399 | in 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. |
| 7589 | This means actually wider on the screen, not character-wise. | 7589 | This means actually wider on the screen, not wider character-wise. |
| 7590 | On text frames, use the heuristic that characters are roughtly twice as | 7590 | On text frames, use the heuristic that characters are roughtly twice as |
| 7591 | tall as they are wide." | 7591 | tall as they are wide." |
| 7592 | (if (display-graphic-p frame) | 7592 | (if (display-graphic-p frame) |