diff options
| -rw-r--r-- | modules/calendar-sync.el | 33 | ||||
| -rw-r--r-- | tests/test-calendar-sync--event-to-org.el | 44 | ||||
| -rw-r--r-- | tests/test-calendar-sync--sanitize-org-body.el | 24 |
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 |
