summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/calendar-sync.el33
-rw-r--r--tests/test-calendar-sync--event-to-org.el44
-rw-r--r--tests/test-calendar-sync--sanitize-org-body.el24
3 files changed, 96 insertions, 5 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index b9cc57f4..06bee213 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -287,6 +287,20 @@ parsed as org headings. Handles multiple levels (e.g. ** becomes --)."
(concat (make-string (length (match-string 1 match)) ?-) " "))
text)))
+(defun calendar-sync--sanitize-org-property-value (text)
+ "Sanitize TEXT for safe inclusion as a single Org property value."
+ (when text
+ (string-trim
+ (replace-regexp-in-string
+ "[[:space:]\n\r]+"
+ " "
+ text))))
+
+(defun calendar-sync--sanitize-org-heading (text)
+ "Sanitize TEXT for safe inclusion as a single Org heading title."
+ (calendar-sync--sanitize-org-property-value
+ (calendar-sync--sanitize-org-body text)))
+
;;; Date Utilities
(defun calendar-sync--add-months (date months)
@@ -1059,7 +1073,8 @@ Cleans text fields (description, location, summary) via `calendar-sync--clean-te
"Convert parsed EVENT plist to org entry string.
Produces property drawer with LOCATION, ORGANIZER, STATUS, URL when present.
Description appears as body text after the drawer."
- (let* ((summary (or (plist-get event :summary) "(No Title)"))
+ (let* ((summary (calendar-sync--sanitize-org-heading
+ (or (plist-get event :summary) "(No Title)")))
(description (plist-get event :description))
(location (plist-get event :location))
(start (plist-get event :start))
@@ -1072,16 +1087,24 @@ Description appears as body text after the drawer."
(props '()))
;; Collect non-nil properties
(when (and location (not (string-empty-p location)))
- (push (format ":LOCATION: %s" location) props))
+ (push (format ":LOCATION: %s"
+ (calendar-sync--sanitize-org-property-value location))
+ props))
(when organizer
(let ((org-name (or (plist-get organizer :cn)
(plist-get organizer :email))))
(when org-name
- (push (format ":ORGANIZER: %s" org-name) props))))
+ (push (format ":ORGANIZER: %s"
+ (calendar-sync--sanitize-org-property-value org-name))
+ props))))
(when (and status (not (string-empty-p status)))
- (push (format ":STATUS: %s" status) props))
+ (push (format ":STATUS: %s"
+ (calendar-sync--sanitize-org-property-value status))
+ props))
(when (and url (not (string-empty-p url)))
- (push (format ":URL: %s" url) props))
+ (push (format ":URL: %s"
+ (calendar-sync--sanitize-org-property-value url))
+ props))
(setq props (nreverse props))
;; Build output
(let ((parts (list timestamp (format "* %s" summary))))
diff --git a/tests/test-calendar-sync--event-to-org.el b/tests/test-calendar-sync--event-to-org.el
index a3dc0106..9f87856f 100644
--- a/tests/test-calendar-sync--event-to-org.el
+++ b/tests/test-calendar-sync--event-to-org.el
@@ -11,6 +11,16 @@
(require 'testutil-calendar-sync)
(require 'calendar-sync)
+(defun test-calendar-sync--count-line-matches (regexp text)
+ "Count lines in TEXT that match REGEXP."
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (let ((count 0))
+ (while (re-search-forward regexp nil t)
+ (setq count (1+ count)))
+ count)))
+
;;; Normal Cases
(ert-deftest test-calendar-sync--event-to-org-normal-all-fields ()
@@ -108,6 +118,40 @@
;; Only the event heading should use *
(should (= 1 (length (split-string result "^\\* " t)))))))
+(ert-deftest test-calendar-sync--event-to-org-boundary-summary-structure-is-flattened ()
+ "Test event summary cannot create additional Org headings."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "* Planning\n** Hidden task"
+ :start start
+ :end end)))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p "^\\* - Planning -- Hidden task$" result))
+ (should-not (string-match-p "^\\*\\* Hidden task" result))
+ (should (= 1 (length (split-string result "^\\* " t)))))))
+
+(ert-deftest test-calendar-sync--event-to-org-boundary-property-structure-is-flattened ()
+ "Test property values cannot create extra drawer lines or close the drawer."
+ (let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
+ (end (test-calendar-sync-time-days-from-now 5 15 0))
+ (event (list :summary "Meeting"
+ :start start
+ :end end
+ :location "Room 1\n:END:\n* Not a real heading"
+ :organizer (list :cn "Jane\n:STATUS: fake"
+ :email "jane@example.com")
+ :status "accepted\n:LOCATION: fake"
+ :url "https://example.com/a\n:PROPERTIES:")))
+ (let ((result (calendar-sync--event-to-org event)))
+ (should (string-match-p ":LOCATION: Room 1 :END: \\* Not a real heading" result))
+ (should (string-match-p ":ORGANIZER: Jane :STATUS: fake" result))
+ (should (string-match-p ":STATUS: accepted :LOCATION: fake" result))
+ (should (string-match-p ":URL: https://example.com/a :PROPERTIES:" result))
+ (should (= 1 (test-calendar-sync--count-line-matches "^:LOCATION:" result)))
+ (should (= 1 (test-calendar-sync--count-line-matches "^:STATUS:" result)))
+ (should (= 1 (test-calendar-sync--count-line-matches "^:END:$" result)))
+ (should-not (string-match-p "^\\* Not a real heading" result)))))
+
(ert-deftest test-calendar-sync--event-to-org-boundary-organizer-email-only ()
"Test organizer without CN shows email."
(let* ((start (test-calendar-sync-time-days-from-now 5 14 0))
diff --git a/tests/test-calendar-sync--sanitize-org-body.el b/tests/test-calendar-sync--sanitize-org-body.el
index c38c277e..636946db 100644
--- a/tests/test-calendar-sync--sanitize-org-body.el
+++ b/tests/test-calendar-sync--sanitize-org-body.el
@@ -70,5 +70,29 @@
"Lone asterisk with space at start of line is sanitized."
(should (equal "- " (calendar-sync--sanitize-org-body "* "))))
+;;; Heading and Property Sanitizers
+
+(ert-deftest test-calendar-sync--sanitize-org-heading-flattens-newlines ()
+ "Heading text should stay on one Org heading line."
+ (should (equal "Planning Agenda"
+ (calendar-sync--sanitize-org-heading "Planning\nAgenda"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-heading-replaces-leading-stars ()
+ "Heading text should not start with Org heading stars."
+ (should (equal "- Planning -- Hidden"
+ (calendar-sync--sanitize-org-heading "* Planning\n** Hidden"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-property-value-flattens-structure ()
+ "Property values should not create extra property drawer lines."
+ (should (equal "Room 1 :END: * Not a heading"
+ (calendar-sync--sanitize-org-property-value
+ "Room 1\n:END:\n* Not a heading"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-property-value-trims-and-collapses ()
+ "Property values should be compact single-line values."
+ (should (equal "alpha beta gamma"
+ (calendar-sync--sanitize-org-property-value
+ " alpha\t beta\n\n gamma "))))
+
(provide 'test-calendar-sync--sanitize-org-body)
;;; test-calendar-sync--sanitize-org-body.el ends here