diff options
| author | Richard Lawrence | 2025-12-28 15:03:09 +0100 |
|---|---|---|
| committer | Stefan Monnier | 2026-01-03 15:31:01 -0500 |
| commit | 74750e269b978b5a18329642d4370fdea2b536c1 (patch) | |
| tree | 80a56816ab893bba0f94bd21bfb20f73f7620b13 | |
| parent | cf20565e636893004c9403be8234f42afb655796 (diff) | |
| download | emacs-74750e269b978b5a18329642d4370fdea2b536c1.tar.gz emacs-74750e269b978b5a18329642d4370fdea2b536c1.zip | |
Some minor code improvements in iCalendar libraryscratch/icalendar
* lisp/calendar/icalendar-mode.el: Update file header.
Fix error display in 'icalendar-errors-mode':
* lisp/calendar/icalendar.el (icalendar-error-regexp): Fix to allow
" *UNFOLDED:" prefix in buffer names. (Extra colon was breaking match.)
(icalendar-format-error): Suppress this prefix preferentially in long
buffer names.
Add declarations to some iCalendar macros:
* lisp/calendar/icalendar-macs.el (icalendar-with-node-value)
(icalendar-with-child-of)
(icalendar-with-param-of)
(icalendar-with-node-children)
(icalendar-with-node-value)
(icalendar-with-param)
* lisp/calendar/icalendar-ast.el (icalendar-make-property)
(icalendar-make-component)
(icalendar-make-node-from-templates): Add (declare ...) forms.
Add `icalendar-trimp' to icalendar-utils.el:
* lisp/calendar/icalendar-utils.el (icalendar-trimp): New function.
* lisp/calendar/diary-icalendar.el
(diary-icalendar-format-entry)
(diary-icalendar-parse-attendees-and-organizer)
(diary-icalendar-parse-location)
(diary-icalendar-parse-url)
(diary-icalendar-parse-uid): Use it to replace diary-icalendar--nonempty.
(diary-icalendar--nonempty): Remove.
Move VTIMEZONE creation to icalendar-recur.el:
The following changes move `diary-icalendar-current-tz-to-vtimezone' and
associated code to icalendar-recur.el. Library users are likely to need
this function, so it makes sense to keep it with other time zone-related
code in that file, instead of having them depend on diary-icalendar.
* lisp/calendar/icalendar-recur.el (icalendar-tz-data-insufficient)
(icalendar-tz-unsupported): New error types.
(icalendar-recur-current-tz-to-vtimezone): Rename from
`diary-icalendar-current-tz-to-vtimezone'; signal new error types.
(icalendar-recur--tz-warning): Rename from `diary-icalendar--tz-warning'.
(icalendar-recur--emacs-local-tzid): Rename from
`diary-icalendar--emacs-local-tzid'.
(icalendar-recur--tz-info-sexp-p): Rename from
`diary-icalendar--tz-info-sexp-p'.
* lisp/calendar/diary-icalendar.el
(diary-icalendar-current-tz-to-vtimezone): Reimplement with
`icalendar-recur-current-tz-to-vtimezone'.
(diary-icalendar--tz-warning)
(diary-icalendar--emacs-local-tzid)
(diary-icalendar--tz-info-sexp-p): Renamed and moved; see above.
(diary-time-zone-export-strategy): Update validation function name.
| -rw-r--r-- | lisp/calendar/diary-icalendar.el | 168 | ||||
| -rw-r--r-- | lisp/calendar/icalendar-ast.el | 9 | ||||
| -rw-r--r-- | lisp/calendar/icalendar-macs.el | 11 | ||||
| -rw-r--r-- | lisp/calendar/icalendar-mode.el | 5 | ||||
| -rw-r--r-- | lisp/calendar/icalendar-recur.el | 157 | ||||
| -rw-r--r-- | lisp/calendar/icalendar-utils.el | 5 | ||||
| -rw-r--r-- | lisp/calendar/icalendar.el | 6 |
7 files changed, 205 insertions, 156 deletions
diff --git a/lisp/calendar/diary-icalendar.el b/lisp/calendar/diary-icalendar.el index 640e063268a..eed53bfd700 100644 --- a/lisp/calendar/diary-icalendar.el +++ b/lisp/calendar/diary-icalendar.el | |||
| @@ -673,32 +673,6 @@ recurring events for several years beyond the start time." | |||
| 673 | :version "31.1" | 673 | :version "31.1" |
| 674 | :type 'integer) | 674 | :type 'integer) |
| 675 | 675 | ||
| 676 | (defun di:-tz-info-sexp-p (_ sexp) | ||
| 677 | "Validate that SEXP gives time zone info like from `calendar-current-time-zone'." | ||
| 678 | (and (listp sexp) | ||
| 679 | (length= sexp 8) | ||
| 680 | (let ((utc-diff (nth 0 sexp)) | ||
| 681 | (dst-offset (nth 1 sexp)) | ||
| 682 | (std-zone (nth 2 sexp)) | ||
| 683 | (dst-zone (nth 3 sexp)) | ||
| 684 | (dst-starts (nth 4 sexp)) | ||
| 685 | (dst-ends (nth 5 sexp)) | ||
| 686 | (dst-starts-time (nth 6 sexp)) | ||
| 687 | (dst-ends-time (nth 7 sexp))) | ||
| 688 | (and | ||
| 689 | (integerp utc-diff) (< (abs utc-diff) (* 60 24)) | ||
| 690 | (integerp dst-offset) (< (abs utc-diff) (* 60 24)) | ||
| 691 | (stringp std-zone) | ||
| 692 | (stringp dst-zone) | ||
| 693 | (or (and (listp dst-starts) (memq 'year (flatten-list dst-starts))) | ||
| 694 | (and (null dst-starts) (equal std-zone dst-zone))) | ||
| 695 | (or (and (listp dst-ends) (memq 'year (flatten-list dst-ends))) | ||
| 696 | (and (null dst-ends) (equal std-zone dst-zone))) | ||
| 697 | (or (and (integerp dst-starts-time) (< (abs dst-starts-time) (* 60 24))) | ||
| 698 | (null dst-starts-time)) | ||
| 699 | (or (and (integerp dst-ends-time) (< (abs dst-ends-time) (* 60 24))) | ||
| 700 | (null dst-ends-time)))))) | ||
| 701 | |||
| 702 | (defcustom di:time-zone-export-strategy | 676 | (defcustom di:time-zone-export-strategy |
| 703 | 'local | 677 | 'local |
| 704 | "Strategy to use for exporting clock times in diary files. | 678 | "Strategy to use for exporting clock times in diary files. |
| @@ -741,7 +715,7 @@ the events you are exporting." | |||
| 741 | (const :tag "Convert local times to UTC" to-utc) | 715 | (const :tag "Convert local times to UTC" to-utc) |
| 742 | (const :tag "Use floating times" floating) | 716 | (const :tag "Use floating times" floating) |
| 743 | (sexp :tag "User-provided TZ information" | 717 | (sexp :tag "User-provided TZ information" |
| 744 | :match di:-tz-info-sexp-p | 718 | :match icr:-tz-info-sexp-p |
| 745 | :type-error | 719 | :type-error |
| 746 | "See `calendar-current-time-zone' for format")) | 720 | "See `calendar-current-time-zone' for format")) |
| 747 | :link '(url-link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5")) | 721 | :link '(url-link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5")) |
| @@ -989,12 +963,6 @@ new code." | |||
| 989 | 963 | ||
| 990 | ;;; Other utilities | 964 | ;;; Other utilities |
| 991 | 965 | ||
| 992 | (defsubst di:-nonempty (s) | ||
| 993 | "Ensure that string S is nonempty once trimmed: return the trimmed S, or nil." | ||
| 994 | (when (and s (stringp s)) | ||
| 995 | (let ((trimmed (string-trim s))) | ||
| 996 | (unless (equal "" trimmed) trimmed)))) | ||
| 997 | |||
| 998 | (defconst di:entry-regexp | 966 | (defconst di:entry-regexp |
| 999 | (rx line-start | 967 | (rx line-start |
| 1000 | (group-n 1 ; first line of entry | 968 | (group-n 1 ; first line of entry |
| @@ -1660,10 +1628,10 @@ Returns a string containing the diary entry." | |||
| 1660 | (if (eq 'icalendar-vjournal component-type) | 1628 | (if (eq 'icalendar-vjournal component-type) |
| 1661 | (mapconcat | 1629 | (mapconcat |
| 1662 | (lambda (node) | 1630 | (lambda (node) |
| 1663 | (di:-nonempty (ical:text-to-string (ical:ast-node-value node)))) | 1631 | (ical:trimp (ical:text-to-string (ical:ast-node-value node)))) |
| 1664 | description-nodes | 1632 | description-nodes |
| 1665 | "\n\n") | 1633 | "\n\n") |
| 1666 | (di:-nonempty description))) | 1634 | (ical:trimp description))) |
| 1667 | (ical-start | 1635 | (ical-start |
| 1668 | (when dtstart | 1636 | (when dtstart |
| 1669 | (if (bound-and-true-p ical-importing) | 1637 | (if (bound-and-true-p ical-importing) |
| @@ -1748,7 +1716,7 @@ Returns a string containing the diary entry." | |||
| 1748 | (when (and dtstart due-dt (bound-and-true-p ical-importing)) | 1716 | (when (and dtstart due-dt (bound-and-true-p ical-importing)) |
| 1749 | (di:format-time-block-sexp dtstart-local due-dt))) | 1717 | (di:format-time-block-sexp dtstart-local due-dt))) |
| 1750 | (ical-importing (bound-and-true-p ical-importing)) | 1718 | (ical-importing (bound-and-true-p ical-importing)) |
| 1751 | (ical-location (or (di:-nonempty location) | 1719 | (ical-location (or (ical:trimp location) |
| 1752 | (when geo (di:format-geo-coordinates geo)))) | 1720 | (when geo (di:format-geo-coordinates geo)))) |
| 1753 | (ical-nonmarking nonmarking) | 1721 | (ical-nonmarking nonmarking) |
| 1754 | (ical-organizer (di:format-attendee organizer-node)) | 1722 | (ical-organizer (di:format-attendee organizer-node)) |
| @@ -1757,11 +1725,11 @@ Returns a string containing the diary entry." | |||
| 1757 | (ical-rrule-sexp | 1725 | (ical-rrule-sexp |
| 1758 | (when (and is-recurring (bound-and-true-p ical-importing)) | 1726 | (when (and is-recurring (bound-and-true-p ical-importing)) |
| 1759 | (di:format-rrule-sexp component))) | 1727 | (di:format-rrule-sexp component))) |
| 1760 | (ical-status (when status (di:-nonempty (downcase status)))) | 1728 | (ical-status (when status (ical:trimp (downcase status)))) |
| 1761 | (ical-summary (di:-nonempty summary)) | 1729 | (ical-summary (ical:trimp summary)) |
| 1762 | (ical-transparency transp) | 1730 | (ical-transparency transp) |
| 1763 | (ical-uid (di:-nonempty uid)) | 1731 | (ical-uid (ical:trimp uid)) |
| 1764 | (ical-url (di:-nonempty url))) | 1732 | (ical-url (ical:trimp url))) |
| 1765 | (with-temp-buffer | 1733 | (with-temp-buffer |
| 1766 | (cl-case (ical:ast-node-type component) | 1734 | (cl-case (ical:ast-node-type component) |
| 1767 | (ical:vevent | 1735 | (ical:vevent |
| @@ -2108,7 +2076,7 @@ parsed as an `icalendar-organizer' node, or otherwise as an | |||
| 2108 | (unless (string-match ":" addr) ; URI scheme already present | 2076 | (unless (string-match ":" addr) ; URI scheme already present |
| 2109 | (setq addr (concat "mailto:" addr))) | 2077 | (setq addr (concat "mailto:" addr))) |
| 2110 | (when cn | 2078 | (when cn |
| 2111 | (setq cn (di:-nonempty cn))) | 2079 | (setq cn (ical:trimp cn))) |
| 2112 | (if (string-match di:organizer-regexp | 2080 | (if (string-match di:organizer-regexp |
| 2113 | (buffer-substring (line-beginning-position) | 2081 | (buffer-substring (line-beginning-position) |
| 2114 | (line-end-position))) | 2082 | (line-end-position))) |
| @@ -2130,7 +2098,7 @@ this node, or nil." | |||
| 2130 | (goto-char (point-min)) | 2098 | (goto-char (point-min)) |
| 2131 | (when (and di:location-regexp | 2099 | (when (and di:location-regexp |
| 2132 | (re-search-forward di:location-regexp nil t)) | 2100 | (re-search-forward di:location-regexp nil t)) |
| 2133 | (ical:make-property ical:location (di:-nonempty (match-string 1))))) | 2101 | (ical:make-property ical:location (ical:trimp (match-string 1))))) |
| 2134 | 2102 | ||
| 2135 | (defun di:parse-class () | 2103 | (defun di:parse-class () |
| 2136 | "Parse `icalendar-class' node from entry. | 2104 | "Parse `icalendar-class' node from entry. |
| @@ -2166,7 +2134,7 @@ Searches the entry in the current restriction for an URL matching | |||
| 2166 | (goto-char (point-min)) | 2134 | (goto-char (point-min)) |
| 2167 | (when (and di:url-regexp | 2135 | (when (and di:url-regexp |
| 2168 | (re-search-forward di:url-regexp nil t)) | 2136 | (re-search-forward di:url-regexp nil t)) |
| 2169 | (ical:make-property ical:url (di:-nonempty (match-string 1))))) | 2137 | (ical:make-property ical:url (ical:trimp (match-string 1))))) |
| 2170 | 2138 | ||
| 2171 | (defun di:parse-uid () | 2139 | (defun di:parse-uid () |
| 2172 | "Parse `icalendar-uid' node from entry. | 2140 | "Parse `icalendar-uid' node from entry. |
| @@ -2177,7 +2145,7 @@ Searches the entry in the current restriction for a UID matching | |||
| 2177 | (goto-char (point-min)) | 2145 | (goto-char (point-min)) |
| 2178 | (when (and di:uid-regexp | 2146 | (when (and di:uid-regexp |
| 2179 | (re-search-forward di:uid-regexp nil t)) | 2147 | (re-search-forward di:uid-regexp nil t)) |
| 2180 | (ical:make-property ical:uid (di:-nonempty (match-string 1))))) | 2148 | (ical:make-property ical:uid (ical:trimp (match-string 1))))) |
| 2181 | 2149 | ||
| 2182 | (defun di:parse-summary-and-description () | 2150 | (defun di:parse-summary-and-description () |
| 2183 | "Parse summary and description nodes from current restriction. | 2151 | "Parse summary and description nodes from current restriction. |
| @@ -3136,112 +3104,18 @@ times according to `diary-icalendar-time-zone-export-strategy'." | |||
| 3136 | 3104 | ||
| 3137 | ;;; Time zone handling during export: | 3105 | ;;; Time zone handling during export: |
| 3138 | 3106 | ||
| 3139 | (defconst di:-tz-warning | ||
| 3140 | "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.") | ||
| 3141 | |||
| 3142 | (defconst di:-emacs-local-tzid | ||
| 3143 | "Emacs_Local_") | ||
| 3144 | |||
| 3145 | (defun di:current-tz-to-vtimezone (&optional tz tzid start-year) | 3107 | (defun di:current-tz-to-vtimezone (&optional tz tzid start-year) |
| 3146 | "Convert TZ to an `icalendar-vtimezone'. | 3108 | "Convert TZ to an `icalendar-vtimezone'. |
| 3147 | 3109 | ||
| 3148 | TZ defaults to the output of `calendar-current-time-zone'; if specified, | 3110 | See `icalendar-recur-current-tz-to-vtimezone' for arguments' meanings. |
| 3149 | it should be a list of the same form as that function returns. | 3111 | This function wraps that one, but signals `icalendar-diary-export-error' |
| 3150 | 3112 | instead if TZ cannot be converted." | |
| 3151 | TZID, if specified, should be a string to identify this time zone; it | 3113 | (condition-case err |
| 3152 | defaults to `diary-icalendar--emacs-local-tzid' plus the name of the | 3114 | (icr:current-tz-to-vtimezone tz tzid start-year) |
| 3153 | standard observance according to `calendar-current-time-zone'. | 3115 | ((ical:tz-insufficient-data ical:tz-unsupported) |
| 3154 | 3116 | (di:signal-export-error | |
| 3155 | START-YEAR, if specified, should be an integer giving the year in which | 3117 | (format "Unable to export time zone data: %s.\n%s." tz |
| 3156 | to start the observances in the time zone. It defaults to 1970." | 3118 | "Check the value of `diary-icalendar-time-zone-export-strategy'"))))) |
| 3157 | (when (and tz (not (di:-tz-info-sexp-p nil tz))) | ||
| 3158 | (di:signal-export-error | ||
| 3159 | (format "Invalid time zone data: %s.\n%s." tz | ||
| 3160 | "Check the value of `diary-icalendar-time-zone-export-strategy'"))) | ||
| 3161 | (let* ((tzdata (or tz (calendar-current-time-zone))) | ||
| 3162 | (std-offset (* 60 (nth 0 tzdata))) | ||
| 3163 | (dst-offset (+ std-offset | ||
| 3164 | (* 60 (nth 1 tzdata)))) | ||
| 3165 | (std-name (nth 2 tzdata)) | ||
| 3166 | (dst-name (nth 3 tzdata)) | ||
| 3167 | (dst-starts (nth 4 tzdata)) | ||
| 3168 | (dst-ends (nth 5 tzdata)) | ||
| 3169 | (dst-start-minutes (nth 6 tzdata)) | ||
| 3170 | (dst-end-minutes (nth 7 tzdata))) | ||
| 3171 | |||
| 3172 | (unless (and std-offset | ||
| 3173 | (or (equal std-name dst-name) | ||
| 3174 | (and dst-starts dst-ends dst-start-minutes dst-end-minutes))) | ||
| 3175 | (di:signal-export-error | ||
| 3176 | "Insufficient time zone information to create VTIMEZONE")) | ||
| 3177 | |||
| 3178 | (if (equal std-name dst-name) | ||
| 3179 | ;; Local time zone doesn't use DST: | ||
| 3180 | (ical:make-vtimezone | ||
| 3181 | (ical:tzid (or tzid (concat di:-emacs-local-tzid std-name))) | ||
| 3182 | (ical:make-standard | ||
| 3183 | (ical:tzname std-name) | ||
| 3184 | (ical:dtstart (ical:make-date-time :year (or start-year 1970) | ||
| 3185 | :month 1 :day 1 | ||
| 3186 | :hour 0 :minute 0 :second 0)) | ||
| 3187 | (ical:tzoffsetfrom std-offset) | ||
| 3188 | (ical:tzoffsetto std-offset) | ||
| 3189 | (ical:comment di:-tz-warning))) | ||
| 3190 | |||
| 3191 | ;; Otherwise we can provide both STANDARD and DAYLIGHT subcomponents: | ||
| 3192 | (let* ((std->dst-rule | ||
| 3193 | (if (eq (car dst-starts) 'calendar-nth-named-day) | ||
| 3194 | `((FREQ YEARLY) | ||
| 3195 | (BYMONTH (,(nth 3 dst-starts))) | ||
| 3196 | (BYDAY (,(cons (nth 2 dst-starts) | ||
| 3197 | (nth 1 dst-starts))))) | ||
| 3198 | ;; The only other rules that `calendar-current-time-zone' | ||
| 3199 | ;; can return are based on the Persian calendar, which we | ||
| 3200 | ;; cannot express in an `icalendar-recur' value, at least | ||
| 3201 | ;; pending an implementation of RFC 7529 | ||
| 3202 | (di:signal-export-error | ||
| 3203 | (format "Unable to export DST rule for current time zone: %s" | ||
| 3204 | dst-starts)))) | ||
| 3205 | (dst-start-date (calendar-dlet ((year (or start-year 1970))) | ||
| 3206 | (eval dst-starts))) | ||
| 3207 | (dst-start | ||
| 3208 | (ical:date-to-date-time dst-start-date | ||
| 3209 | :hour (/ dst-start-minutes 60) | ||
| 3210 | :minute (mod dst-start-minutes 60) | ||
| 3211 | :second 0)) | ||
| 3212 | (dst->std-rule | ||
| 3213 | (if (eq (car dst-ends) 'calendar-nth-named-day) | ||
| 3214 | `((FREQ YEARLY) | ||
| 3215 | (BYMONTH (,(nth 3 dst-ends))) | ||
| 3216 | (BYDAY (,(cons (nth 2 dst-ends) | ||
| 3217 | (nth 1 dst-ends))))) | ||
| 3218 | (di:signal-export-error | ||
| 3219 | (format "Unable to export DST rule for current time zone: %s" | ||
| 3220 | dst-ends)))) | ||
| 3221 | (std-start-date (calendar-dlet ((year (1- (or start-year 1970)))) | ||
| 3222 | (eval dst-ends))) | ||
| 3223 | (std-start | ||
| 3224 | (ical:date-to-date-time std-start-date | ||
| 3225 | :hour (/ dst-end-minutes 60) | ||
| 3226 | :minute (mod dst-end-minutes 60) | ||
| 3227 | :second 0))) | ||
| 3228 | |||
| 3229 | (ical:make-vtimezone | ||
| 3230 | (ical:tzid (or tzid (concat di:-emacs-local-tzid std-name))) | ||
| 3231 | (ical:make-standard | ||
| 3232 | (ical:tzname std-name) | ||
| 3233 | (ical:dtstart std-start) | ||
| 3234 | (ical:rrule dst->std-rule) | ||
| 3235 | (ical:tzoffsetfrom dst-offset) | ||
| 3236 | (ical:tzoffsetto std-offset) | ||
| 3237 | (ical:comment di:-tz-warning)) | ||
| 3238 | (ical:make-daylight | ||
| 3239 | (ical:tzname dst-name) | ||
| 3240 | (ical:dtstart dst-start) | ||
| 3241 | (ical:rrule std->dst-rule) | ||
| 3242 | (ical:tzoffsetfrom std-offset) | ||
| 3243 | (ical:tzoffsetto dst-offset) | ||
| 3244 | (ical:comment di:-tz-warning))))))) | ||
| 3245 | 3119 | ||
| 3246 | ;;; Parsing complete diary entries: | 3120 | ;;; Parsing complete diary entries: |
| 3247 | 3121 | ||
diff --git a/lisp/calendar/icalendar-ast.el b/lisp/calendar/icalendar-ast.el index 795d9dde65a..a84e28d36c1 100644 --- a/lisp/calendar/icalendar-ast.el +++ b/lisp/calendar/icalendar-ast.el | |||
| @@ -490,7 +490,8 @@ The resulting syntax node is checked for validity by | |||
| 490 | `icalendar-ast-node-valid-p' before it is returned." | 490 | `icalendar-ast-node-valid-p' before it is returned." |
| 491 | ;; TODO: support `ical:other-property', maybe like | 491 | ;; TODO: support `ical:other-property', maybe like |
| 492 | ;; (ical:other-property "X-NAME" value ...) | 492 | ;; (ical:other-property "X-NAME" value ...) |
| 493 | (declare (debug (symbolp form form &rest form))) | 493 | (declare (debug (symbolp form form &rest form)) |
| 494 | (indent 2)) | ||
| 494 | (unless (ical:property-type-symbol-p type) | 495 | (unless (ical:property-type-symbol-p type) |
| 495 | (error "Not an iCalendar property type: %s" type)) | 496 | (error "Not an iCalendar property type: %s" type)) |
| 496 | (let ((value-types (cons (get type 'ical:default-type) | 497 | (let ((value-types (cons (get type 'ical:default-type) |
| @@ -553,7 +554,8 @@ properties. | |||
| 553 | 554 | ||
| 554 | The resulting syntax node is checked for validity by | 555 | The resulting syntax node is checked for validity by |
| 555 | `icalendar-ast-node-valid-p' before it is returned." | 556 | `icalendar-ast-node-valid-p' before it is returned." |
| 556 | (declare (debug (symbolp form &rest form))) | 557 | (declare (debug (symbolp form &rest form)) |
| 558 | (indent 1)) | ||
| 557 | ;; TODO: support `ical:other-component', maybe like | 559 | ;; TODO: support `ical:other-component', maybe like |
| 558 | ;; (ical:other-component (:x-name "X-NAME") templates ...) | 560 | ;; (ical:other-component (:x-name "X-NAME") templates ...) |
| 559 | (unless (ical:component-type-symbol-p type) | 561 | (unless (ical:component-type-symbol-p type) |
| @@ -660,7 +662,8 @@ For example, an iCalendar VEVENT could be written like this: | |||
| 660 | 662 | ||
| 661 | Before the constructed node is returned, it is validated by | 663 | Before the constructed node is returned, it is validated by |
| 662 | `icalendar-ast-node-valid-p'." | 664 | `icalendar-ast-node-valid-p'." |
| 663 | (declare (debug (symbolp form &rest form))) | 665 | (declare (debug (symbolp form &rest form)) |
| 666 | (indent 1)) | ||
| 664 | (cond | 667 | (cond |
| 665 | ((not (ical:type-symbol-p type)) | 668 | ((not (ical:type-symbol-p type)) |
| 666 | (error "Not an iCalendar type symbol: %s" type)) | 669 | (error "Not an iCalendar type symbol: %s" type)) |
diff --git a/lisp/calendar/icalendar-macs.el b/lisp/calendar/icalendar-macs.el index fe99cef14bc..852b48012a7 100644 --- a/lisp/calendar/icalendar-macs.el +++ b/lisp/calendar/icalendar-macs.el | |||
| @@ -830,7 +830,8 @@ Each binding in BINDINGS should be a list of one of the following forms: | |||
| 830 | nodes), or the :value-nodes themselves (if they are not). | 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 | 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." | 832 | takes multiple values, or the plural keywords with a TYPE that does not." |
| 833 | (declare (indent 2)) | 833 | (declare (debug (symbolp form form &rest form)) |
| 834 | (indent 2)) | ||
| 834 | ;; Static checks on the bindings prevent various annoying bugs: | 835 | ;; Static checks on the bindings prevent various annoying bugs: |
| 835 | (dolist (b bindings) | 836 | (dolist (b bindings) |
| 836 | (let ((type (car b)) | 837 | (let ((type (car b)) |
| @@ -1003,6 +1004,8 @@ is equivalent to | |||
| 1003 | 1004 | ||
| 1004 | BINDINGS are passed on to `icalendar-with-node-children' and will be | 1005 | BINDINGS are passed on to `icalendar-with-node-children' and will be |
| 1005 | available in BODY; see its docstring for their form." | 1006 | available in BODY; see its docstring for their form." |
| 1007 | (declare (debug (symbolp form &optional form &rest form)) | ||
| 1008 | (indent 2)) | ||
| 1006 | (let ((vn (gensym "icalendar-node")) | 1009 | (let ((vn (gensym "icalendar-node")) |
| 1007 | (val (gensym "icalendar-value")) | 1010 | (val (gensym "icalendar-value")) |
| 1008 | (is-list (gensym "is-list"))) | 1011 | (is-list (gensym "is-list"))) |
| @@ -1066,6 +1069,8 @@ node's value. | |||
| 1066 | If PARAMETER's value is not a syntax node, then `value' is bound | 1069 | If PARAMETER's value is not a syntax node, then `value' is bound |
| 1067 | directly to PARAMETER's value, and `value-type' and `value-node' are | 1070 | directly to PARAMETER's value, and `value-type' and `value-node' are |
| 1068 | bound to nil." | 1071 | bound to nil." |
| 1072 | (declare (debug (symbolp form &rest form)) | ||
| 1073 | (indent 1)) | ||
| 1069 | `(ical:with-node-value ,parameter nil ,@body)) | 1074 | `(ical:with-node-value ,parameter nil ,@body)) |
| 1070 | 1075 | ||
| 1071 | (defmacro ical:with-child-of (node type &optional bindings &rest body) | 1076 | (defmacro ical:with-child-of (node type &optional bindings &rest body) |
| @@ -1084,6 +1089,8 @@ is equivalent to | |||
| 1084 | (icalendar-with-child-of some-node some-type nil value) | 1089 | (icalendar-with-child-of some-node some-type nil value) |
| 1085 | 1090 | ||
| 1086 | See `icalendar-with-node-children' for the form of BINDINGS." | 1091 | See `icalendar-with-node-children' for the form of BINDINGS." |
| 1092 | (declare (debug (symbolp form form &optional form &rest form)) | ||
| 1093 | (indent 3)) | ||
| 1087 | (let ((child (gensym "icalendar-node"))) | 1094 | (let ((child (gensym "icalendar-node"))) |
| 1088 | `(let ((,child (ical:ast-node-first-child-of ,type ,node))) | 1095 | `(let ((,child (ical:ast-node-first-child-of ,type ,node))) |
| 1089 | (ical:with-node-value ,child ,bindings ,@body)))) | 1096 | (ical:with-node-value ,child ,bindings ,@body)))) |
| @@ -1116,6 +1123,8 @@ symbol `value'; thus | |||
| 1116 | (icalendar-with-param-of some-property some-type) | 1123 | (icalendar-with-param-of some-property some-type) |
| 1117 | is equivalent to | 1124 | is equivalent to |
| 1118 | (icalendar-with-param-of some-property some-type nil value)" | 1125 | (icalendar-with-param-of some-property some-type nil value)" |
| 1126 | (declare (debug (symbolp form form &rest form)) | ||
| 1127 | (indent 2)) | ||
| 1119 | `(ical:with-child-of ,node ,type nil ,@body)) | 1128 | `(ical:with-child-of ,node ,type nil ,@body)) |
| 1120 | 1129 | ||
| 1121 | (provide 'icalendar-macs) | 1130 | (provide 'icalendar-macs) |
diff --git a/lisp/calendar/icalendar-mode.el b/lisp/calendar/icalendar-mode.el index 2fc2aec44ff..c68a912d296 100644 --- a/lisp/calendar/icalendar-mode.el +++ b/lisp/calendar/icalendar-mode.el | |||
| @@ -1,10 +1,12 @@ | |||
| 1 | ;;; icalendar-mode.el --- Major mode for iCalendar format -*- lexical-binding: t; -*- | 1 | ;;; icalendar-mode.el --- Major mode for iCalendar format -*- lexical-binding: t; -*- |
| 2 | ;;; | 2 | ;;; |
| 3 | 3 | ||
| 4 | ;; Copyright (C) 2024 Richard Lawrence | 4 | ;; Copyright (C) 2024 Free Software Foundation, Inc. |
| 5 | 5 | ||
| 6 | ;; Author: Richard Lawrence <rwl@recursewithless.net> | 6 | ;; Author: Richard Lawrence <rwl@recursewithless.net> |
| 7 | ;; Created: October 2024 | ||
| 7 | ;; Keywords: calendar | 8 | ;; Keywords: calendar |
| 9 | ;; Human-Keywords: calendar, iCalendar | ||
| 8 | 10 | ||
| 9 | ;; This file is part of GNU Emacs. | 11 | ;; This file is part of GNU Emacs. |
| 10 | 12 | ||
| @@ -598,7 +600,6 @@ folding and syntax highlighting. Consider using `visual-line-mode' in | |||
| 598 | ;; TODO: mode-specific menu and context menus | 600 | ;; TODO: mode-specific menu and context menus |
| 599 | ;; TODO: eldoc integration | 601 | ;; TODO: eldoc integration |
| 600 | ;; TODO: completion of keywords | 602 | ;; TODO: completion of keywords |
| 601 | ;; TODO: hook for folding in change-major-mode-hook? | ||
| 602 | (progn | 603 | (progn |
| 603 | (setq font-lock-defaults '(ical:font-lock-keywords nil t)))) | 604 | (setq font-lock-defaults '(ical:font-lock-keywords nil t)))) |
| 604 | 605 | ||
diff --git a/lisp/calendar/icalendar-recur.el b/lisp/calendar/icalendar-recur.el index 2f9045f278e..e3bee0923a9 100644 --- a/lisp/calendar/icalendar-recur.el +++ b/lisp/calendar/icalendar-recur.el | |||
| @@ -76,6 +76,7 @@ | |||
| 76 | (require 'icalendar-utils) | 76 | (require 'icalendar-utils) |
| 77 | (require 'cl-lib) | 77 | (require 'cl-lib) |
| 78 | (require 'calendar) | 78 | (require 'calendar) |
| 79 | (require 'cal-dst) | ||
| 79 | (require 'simple) | 80 | (require 'simple) |
| 80 | (require 'seq) | 81 | (require 'seq) |
| 81 | (eval-when-compile '(require 'icalendar-macs)) | 82 | (eval-when-compile '(require 'icalendar-macs)) |
| @@ -1478,6 +1479,14 @@ UTC offsets local to that time zone." | |||
| 1478 | (define-error 'ical:tz-no-observance "No observance found for date-time" | 1479 | (define-error 'ical:tz-no-observance "No observance found for date-time" |
| 1479 | 'ical:error) | 1480 | 'ical:error) |
| 1480 | 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 | |||
| 1481 | ;; In RFC5545 Section 3.3.10, we read: "If the computed local start time | 1490 | ;; In RFC5545 Section 3.3.10, we read: "If the computed local start time |
| 1482 | ;; of a recurrence instance does not exist ... the time of the | 1491 | ;; of a recurrence instance does not exist ... the time of the |
| 1483 | ;; recurrence instance is interpreted in the same manner as an explicit | 1492 | ;; recurrence instance is interpreted in the same manner as an explicit |
| @@ -1983,7 +1992,153 @@ observance." | |||
| 1983 | (observance (car obs/onset))) | 1992 | (observance (car obs/onset))) |
| 1984 | (ical:with-property-of observance 'ical:tzname))) | 1993 | (ical:with-property-of observance 'ical:tzname))) |
| 1985 | 1994 | ||
| 1986 | 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))) | ||
| 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))) | ||
| 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))))))) | ||
| 1987 | 2142 | ||
| 1988 | (provide 'icalendar-recur) | 2143 | (provide 'icalendar-recur) |
| 1989 | 2144 | ||
diff --git a/lisp/calendar/icalendar-utils.el b/lisp/calendar/icalendar-utils.el index 28d98304d78..3f8e9d085c2 100644 --- a/lisp/calendar/icalendar-utils.el +++ b/lisp/calendar/icalendar-utils.el | |||
| @@ -82,6 +82,11 @@ COMPONENT can be any component node." | |||
| 82 | (ical:with-param-of property 'ical:tzidparam nil value)) | 82 | (ical:with-param-of property 'ical:tzidparam nil value)) |
| 83 | 83 | ||
| 84 | ;; String manipulation | 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)))) | ||
| 85 | 90 | ||
| 86 | (defun ical:strip-mailto (s) | 91 | (defun ical:strip-mailto (s) |
| 87 | "Remove \"mailto:\" case-insensitively from the start of S." | 92 | "Remove \"mailto:\" case-insensitively from the start of S." |
diff --git a/lisp/calendar/icalendar.el b/lisp/calendar/icalendar.el index d617e1eb8c5..2e00408564e 100644 --- a/lisp/calendar/icalendar.el +++ b/lisp/calendar/icalendar.el | |||
| @@ -405,7 +405,7 @@ ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'." | |||
| 405 | (group "(" | 405 | (group "(" |
| 406 | (or (group-n 3 "ERROR") (group-n 4 "WARNING") (group-n 5 "INFO")) | 406 | (or (group-n 3 "ERROR") (group-n 4 "WARNING") (group-n 5 "INFO")) |
| 407 | ")")) | 407 | ")")) |
| 408 | (group-n 1 (zero-or-more (not ":"))) ":" | 408 | (group-n 1 (zero-or-one " *UNFOLDED:") (zero-or-more (not ":"))) ":" |
| 409 | (zero-or-one (group-n 2 (one-or-more digit))) | 409 | (zero-or-one (group-n 2 (one-or-more digit))) |
| 410 | ":") | 410 | ":") |
| 411 | "Regexp to match iCalendar errors. | 411 | "Regexp to match iCalendar errors. |
| @@ -455,7 +455,9 @@ data in ERROR-PLIST, if `icalendar-debug-level' is | |||
| 455 | error-plist)))) | 455 | error-plist)))) |
| 456 | ;; Make sure buffer name doesn't take too much space: | 456 | ;; Make sure buffer name doesn't take too much space: |
| 457 | (when (< 8 (length name)) | 457 | (when (< 8 (length name)) |
| 458 | (put-text-property 9 (length name) 'display "..." name)) | 458 | (if (equal " *UNFOLDED:" (substring name 0 11)) |
| 459 | (put-text-property 0 11 'display "..." name) | ||
| 460 | (put-text-property 9 (length name) 'display "..." name))) | ||
| 459 | (format "(%s)%s:%s: %s\n%s" level name pos message debug-info))) | 461 | (format "(%s)%s:%s: %s\n%s" level name pos message debug-info))) |
| 460 | 462 | ||
| 461 | (defun ical:handle-generic-error (err-data &optional err-buffer) | 463 | (defun ical:handle-generic-error (err-data &optional err-buffer) |