summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/calendar-sync.el15
-rw-r--r--tests/test-calendar-sync--event-to-org.el11
-rw-r--r--tests/test-calendar-sync--sanitize-org-body.el73
3 files changed, 94 insertions, 5 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index 06248531..022aff80 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -275,6 +275,17 @@ Returns nil for nil input. Returns empty string for whitespace-only input."
(when text
(string-trim (calendar-sync--strip-html (calendar-sync--unescape-ics-text text)))))
+(defun calendar-sync--sanitize-org-body (text)
+ "Sanitize TEXT for safe inclusion as org body content.
+Replaces leading asterisks with dashes to prevent lines from being
+parsed as org headings. Handles multiple levels (e.g. ** becomes --)."
+ (when text
+ (replace-regexp-in-string
+ "^\\(\\*+\\) "
+ (lambda (match)
+ (concat (make-string (length (match-string 1 match)) ?-) " "))
+ text)))
+
;;; Date Utilities
(defun calendar-sync--add-months (date months)
@@ -1175,9 +1186,9 @@ Description appears as body text after the drawer."
(dolist (prop props)
(push prop parts))
(push ":END:" parts))
- ;; Add description as body text
+ ;; Add description as body text (sanitized to prevent org heading conflicts)
(when (and description (not (string-empty-p description)))
- (push description parts))
+ (push (calendar-sync--sanitize-org-body description) parts))
(string-join (nreverse parts) "\n"))))
(defun calendar-sync--event-start-time (event)
diff --git a/tests/test-calendar-sync--event-to-org.el b/tests/test-calendar-sync--event-to-org.el
index 96ce49d5..a3dc0106 100644
--- a/tests/test-calendar-sync--event-to-org.el
+++ b/tests/test-calendar-sync--event-to-org.el
@@ -92,7 +92,7 @@
(should (string-match-p ":END:" result)))))
(ert-deftest test-calendar-sync--event-to-org-boundary-description-with-asterisks ()
- "Test event description containing org-special asterisks."
+ "Test event description containing org-special asterisks are sanitized."
(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"
@@ -100,8 +100,13 @@
:end end
:description "* agenda item 1\n** sub-item")))
(let ((result (calendar-sync--event-to-org event)))
- ;; Description should be present
- (should (string-match-p "agenda item" result)))))
+ ;; Description content should be present
+ (should (string-match-p "agenda item" result))
+ ;; Leading asterisks replaced with dashes to prevent org heading conflicts
+ (should (string-match-p "^- agenda item 1" result))
+ (should (string-match-p "^-- sub-item" result))
+ ;; Only the event heading should use *
+ (should (= 1 (length (split-string result "^\\* " t)))))))
(ert-deftest test-calendar-sync--event-to-org-boundary-organizer-email-only ()
"Test organizer without CN shows email."
diff --git a/tests/test-calendar-sync--sanitize-org-body.el b/tests/test-calendar-sync--sanitize-org-body.el
new file mode 100644
index 00000000..c85e763c
--- /dev/null
+++ b/tests/test-calendar-sync--sanitize-org-body.el
@@ -0,0 +1,73 @@
+;;; test-calendar-sync--sanitize-org-body.el --- Tests for org body sanitization -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for calendar-sync--sanitize-org-body.
+;; Ensures description text with org-special syntax (leading asterisks)
+;; is escaped to prevent corruption of the org file structure.
+
+;;; Code:
+
+(require 'ert)
+(require 'calendar-sync)
+
+;;; Normal Cases
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-single-asterisk ()
+ "Single leading asterisk replaced with dash."
+ (should (equal "- item one" (calendar-sync--sanitize-org-body "* item one"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-double-asterisk ()
+ "Double leading asterisks replaced with double dashes."
+ (should (equal "-- sub-item" (calendar-sync--sanitize-org-body "** sub-item"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-triple-asterisk ()
+ "Triple leading asterisks replaced with triple dashes."
+ (should (equal "--- deep item" (calendar-sync--sanitize-org-body "*** deep item"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-multiline ()
+ "Multiple lines with asterisks all get sanitized."
+ (let ((input "Format:\n* What did you do yesterday?\n* What are you doing today?\n* Is anything in your way?")
+ (expected "Format:\n- What did you do yesterday?\n- What are you doing today?\n- Is anything in your way?"))
+ (should (equal expected (calendar-sync--sanitize-org-body input)))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-mixed-lines ()
+ "Only lines starting with asterisks are changed."
+ (let ((input "Normal line\n* Bullet line\nAnother normal line"))
+ (should (equal "Normal line\n- Bullet line\nAnother normal line"
+ (calendar-sync--sanitize-org-body input)))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-normal-mixed-levels ()
+ "Lines with different asterisk counts are each handled."
+ (let ((input "* Top\n** Middle\n*** Bottom"))
+ (should (equal "- Top\n-- Middle\n--- Bottom"
+ (calendar-sync--sanitize-org-body input)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-nil-input ()
+ "Nil input returns nil."
+ (should (null (calendar-sync--sanitize-org-body nil))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-empty-string ()
+ "Empty string returns empty string."
+ (should (equal "" (calendar-sync--sanitize-org-body ""))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-no-asterisks ()
+ "Text without leading asterisks is returned unchanged."
+ (let ((input "Just a normal description\nwith multiple lines"))
+ (should (equal input (calendar-sync--sanitize-org-body input)))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-mid-line ()
+ "Asterisks not at line start are left alone."
+ (should (equal "Use * for emphasis" (calendar-sync--sanitize-org-body "Use * for emphasis"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-no-space ()
+ "Asterisk at line start without trailing space is not a heading — left alone."
+ (should (equal "*bold text*" (calendar-sync--sanitize-org-body "*bold text*"))))
+
+(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-only ()
+ "Lone asterisk with space at start of line is sanitized."
+ (should (equal "- " (calendar-sync--sanitize-org-body "* "))))
+
+(provide 'test-calendar-sync--sanitize-org-body)
+;;; test-calendar-sync--sanitize-org-body.el ends here