aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Lawrence2025-12-28 15:03:09 +0100
committerStefan Monnier2026-01-03 15:31:01 -0500
commit74750e269b978b5a18329642d4370fdea2b536c1 (patch)
tree80a56816ab893bba0f94bd21bfb20f73f7620b13
parentcf20565e636893004c9403be8234f42afb655796 (diff)
downloademacs-scratch/icalendar.tar.gz
emacs-scratch/icalendar.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.el168
-rw-r--r--lisp/calendar/icalendar-ast.el9
-rw-r--r--lisp/calendar/icalendar-macs.el11
-rw-r--r--lisp/calendar/icalendar-mode.el5
-rw-r--r--lisp/calendar/icalendar-recur.el157
-rw-r--r--lisp/calendar/icalendar-utils.el5
-rw-r--r--lisp/calendar/icalendar.el6
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
3148TZ defaults to the output of `calendar-current-time-zone'; if specified, 3110See `icalendar-recur-current-tz-to-vtimezone' for arguments' meanings.
3149it should be a list of the same form as that function returns. 3111This function wraps that one, but signals `icalendar-diary-export-error'
3150 3112instead if TZ cannot be converted."
3151TZID, if specified, should be a string to identify this time zone; it 3113 (condition-case err
3152defaults to `diary-icalendar--emacs-local-tzid' plus the name of the 3114 (icr:current-tz-to-vtimezone tz tzid start-year)
3153standard observance according to `calendar-current-time-zone'. 3115 ((ical:tz-insufficient-data ical:tz-unsupported)
3154 3116 (di:signal-export-error
3155START-YEAR, if specified, should be an integer giving the year in which 3117 (format "Unable to export time zone data: %s.\n%s." tz
3156to 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
554The resulting syntax node is checked for validity by 555The 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
661Before the constructed node is returned, it is validated by 663Before 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
1004BINDINGS are passed on to `icalendar-with-node-children' and will be 1005BINDINGS are passed on to `icalendar-with-node-children' and will be
1005available in BODY; see its docstring for their form." 1006available 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.
1066If PARAMETER's value is not a syntax node, then `value' is bound 1069If PARAMETER's value is not a syntax node, then `value' is bound
1067directly to PARAMETER's value, and `value-type' and `value-node' are 1070directly to PARAMETER's value, and `value-type' and `value-node' are
1068bound to nil." 1071bound 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
1086See `icalendar-with-node-children' for the form of BINDINGS." 1091See `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)
1117is equivalent to 1124is 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
2030TZ defaults to the output of `calendar-current-time-zone'; if specified,
2031it should be a list of the same form as that function returns.
2032Depending on TZ, this function might signal the following errors:
2033
2034`icalendar-tz-data-insufficient' if the data in TZ is not complete
2035 enough to determine time zone rules.
2036`icalendar-tz-unsupported' if the data in TZ cannot be expressed as an
2037 RFC5545 `icalendar-rrule' property.
2038
2039TZID, if specified, should be a string to identify this time zone; it
2040defaults to `icalendar-recur--emacs-local-tzid' plus the name of the
2041standard observance according to `calendar-current-time-zone'.
2042
2043START-YEAR, if specified, should be an integer giving the year in which
2044to start the observances in the time zone. It defaults to 1970."
2045 (when (and tz (not (icr:-tz-info-sexp-p nil tz)))
2046 (signal 'ical:tz-data-insufficient
2047 (list :tz tz
2048 :level 2
2049 :message
2050 "Badly formed TZ data; see `calendar-current-time-zone'")))
2051 (let* ((tzdata (or tz (calendar-current-time-zone)))
2052 (std-offset (* 60 (nth 0 tzdata)))
2053 (dst-offset (+ std-offset
2054 (* 60 (nth 1 tzdata))))
2055 (std-name (nth 2 tzdata))
2056 (dst-name (nth 3 tzdata))
2057 (dst-starts (nth 4 tzdata))
2058 (dst-ends (nth 5 tzdata))
2059 (dst-start-minutes (nth 6 tzdata))
2060 (dst-end-minutes (nth 7 tzdata)))
2061
2062 (unless (and std-offset
2063 (or (equal std-name dst-name)
2064 (and dst-starts dst-ends dst-start-minutes dst-end-minutes)))
2065 (signal 'ical:tz-data-insufficient
2066 (list :tz tz :level 2
2067 :message "Unable to create VTIMEZONE from TZ")))
2068
2069 (if (equal std-name dst-name)
2070 ;; Local time zone doesn't use DST:
2071 (ical:make-vtimezone
2072 (ical:tzid (or tzid (concat icr:-emacs-local-tzid std-name)))
2073 (ical:make-standard
2074 (ical:tzname std-name)
2075 (ical:dtstart (ical:make-date-time :year (or start-year 1970)
2076 :month 1 :day 1
2077 :hour 0 :minute 0 :second 0))
2078 (ical:tzoffsetfrom std-offset)
2079 (ical:tzoffsetto std-offset)
2080 (ical:comment icr:-tz-warning)))
2081
2082 ;; Otherwise we can provide both STANDARD and DAYLIGHT subcomponents:
2083 (let* ((std->dst-rule
2084 (if (eq (car dst-starts) 'calendar-nth-named-day)
2085 `((FREQ YEARLY)
2086 (BYMONTH (,(nth 3 dst-starts)))
2087 (BYDAY (,(cons (nth 2 dst-starts)
2088 (nth 1 dst-starts)))))
2089 ;; The only other rules that `calendar-current-time-zone'
2090 ;; can return are based on the Persian calendar, which we
2091 ;; cannot express in an `icalendar-recur' value, at least
2092 ;; pending an implementation of RFC 7529
2093 (signal 'ical:tz-unsupported
2094 (list :tz tz
2095 :level 2
2096 :message
2097 (format "Unable to export DST rule for time zone: %s"
2098 dst-starts)))))
2099 (dst-start-date (calendar-dlet ((year (or start-year 1970)))
2100 (eval dst-starts)))
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)