summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-05 16:10:09 -0600
committerCraig Jennings <c@cjennings.net>2026-02-05 16:10:09 -0600
commit00ddf74b232b0762baa7826e62f6765d087041fb (patch)
treef1afb177b5801acb60932657eed5c06298e73245
parentb7cb1c51e5663419344d8b55766635801f3ee4c8 (diff)
fix(calendar-sync): fix heading order, continuation lines, and exception text cleaningHEADmain
Three bugs found during manual verification of calendar sync output: 1. Heading/timestamp order reversed in event-to-org โ€” nreverse pattern put timestamp before the org heading. Swap initial list order. 2. get-property continuation line regex broken โ€” the ^ anchor in "^\n[ \t]" prevented matching at the correct position, truncating long DESCRIPTION values at the first ICS line fold. Remove anchor, add explicit position check (matching get-all-property-lines pattern). 3. collect-recurrence-exceptions didn't clean text โ€” exception instances got raw ICS text (literal \n, HTML tags) replacing the cleaned base event text. Wrap summary/description/location in clean-text.
-rw-r--r--modules/calendar-sync.el16
-rw-r--r--tests/test-calendar-sync--event-to-org.el6
-rw-r--r--tests/test-calendar-sync--get-property.el20
3 files changed, 35 insertions, 7 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index fadad6c0..06248531 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -413,9 +413,12 @@ Each exception plist contains :recurrence-id (parsed), :start, :end, :summary, e
(end-tzid (calendar-sync--extract-tzid dtend-line))
(start-parsed (calendar-sync--parse-timestamp dtstart start-tzid))
(end-parsed (and dtend (calendar-sync--parse-timestamp dtend end-tzid)))
- (summary (calendar-sync--get-property event-str "SUMMARY"))
- (description (calendar-sync--get-property event-str "DESCRIPTION"))
- (location (calendar-sync--get-property event-str "LOCATION")))
+ (summary (calendar-sync--clean-text
+ (calendar-sync--get-property event-str "SUMMARY")))
+ (description (calendar-sync--clean-text
+ (calendar-sync--get-property event-str "DESCRIPTION")))
+ (location (calendar-sync--clean-text
+ (calendar-sync--get-property event-str "LOCATION"))))
(when (and recurrence-id-parsed start-parsed)
;; Convert RECURRENCE-ID to local time
;; Handle: UTC (Z suffix), TZID, or assume local
@@ -658,9 +661,10 @@ Returns nil if property not found."
(when (string-match (format "^%s[^:\n]*:\\(.*\\)$" (regexp-quote property)) event)
(let ((value (match-string 1 event))
(start (match-end 0)))
- ;; Handle continuation lines (start with space or tab)
+ ;; Handle continuation lines (RFC 5545 ยง3.1: folded lines start with space or tab)
(while (and (< start (length event))
- (string-match "^\n[ \t]\\(.*\\)$" event start))
+ (string-match "\n[ \t]\\([^\n]*\\)" event start)
+ (= (match-beginning 0) start))
(setq value (concat value (match-string 1 event)))
(setq start (match-end 0)))
value)))
@@ -1164,7 +1168,7 @@ Description appears as body text after the drawer."
(push (format ":URL: %s" url) props))
(setq props (nreverse props))
;; Build output
- (let ((parts (list (format "* %s" summary) timestamp)))
+ (let ((parts (list timestamp (format "* %s" summary))))
;; Add property drawer if any properties exist
(when props
(push ":PROPERTIES:" parts)
diff --git a/tests/test-calendar-sync--event-to-org.el b/tests/test-calendar-sync--event-to-org.el
index e6609e20..96ce49d5 100644
--- a/tests/test-calendar-sync--event-to-org.el
+++ b/tests/test-calendar-sync--event-to-org.el
@@ -26,7 +26,11 @@
:status "accepted"
:url "https://meet.google.com/abc-defg-hij")))
(let ((result (calendar-sync--event-to-org event)))
- (should (string-match-p "\\* Team Standup" result))
+ ;; Verify heading comes before timestamp
+ (should (string-match "\\* Team Standup" result))
+ (let ((heading-pos (match-beginning 0)))
+ (should (string-match "<[0-9]" result))
+ (should (< heading-pos (match-beginning 0))))
(should (string-match-p ":PROPERTIES:" result))
(should (string-match-p ":LOCATION: Conference Room A" result))
(should (string-match-p ":ORGANIZER: John Smith" result))
diff --git a/tests/test-calendar-sync--get-property.el b/tests/test-calendar-sync--get-property.el
index 8b71f8e3..20551860 100644
--- a/tests/test-calendar-sync--get-property.el
+++ b/tests/test-calendar-sync--get-property.el
@@ -133,6 +133,26 @@
(should (equal (calendar-sync--get-property event "DESCRIPTION") "Tasks: setup; review; deploy")))
(test-calendar-sync--get-property-teardown)))
+(ert-deftest test-calendar-sync--get-property-boundary-continuation-lines-joined ()
+ "Test extracting property value with RFC 5545 continuation lines.
+Folded lines start with a space or tab and should be joined."
+ (test-calendar-sync--get-property-setup)
+ (unwind-protect
+ (let ((event "BEGIN:VEVENT\nDESCRIPTION:This is a long\n description that spans\n multiple lines\nSUMMARY:Test\nEND:VEVENT"))
+ (should (equal (calendar-sync--get-property event "DESCRIPTION")
+ "This is a longdescription that spansmultiple lines")))
+ (test-calendar-sync--get-property-teardown)))
+
+(ert-deftest test-calendar-sync--get-property-boundary-continuation-with-html ()
+ "Test that HTML tags split across continuation lines are fully captured."
+ (test-calendar-sync--get-property-setup)
+ (unwind-protect
+ (let ((event "BEGIN:VEVENT\nDESCRIPTION:Created by <a href=\"https://ex\n ample.com\">link</a> here\nEND:VEVENT"))
+ (let ((value (calendar-sync--get-property event "DESCRIPTION")))
+ (should (string-match-p "example.com" value))
+ (should (string-match-p "</a>" value))))
+ (test-calendar-sync--get-property-teardown)))
+
;;; Error Cases
(ert-deftest test-calendar-sync--get-property-error-missing-property-returns-nil ()