diff --git a/pkg/routes/caldav/description_test.go b/pkg/routes/caldav/description_test.go
new file mode 100644
index 000000000..f70288d35
--- /dev/null
+++ b/pkg/routes/caldav/description_test.go
@@ -0,0 +1,88 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-present Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
Hello world
` + vTask := &models.Task{Description: "Hello **world**"} + + require.NoError(t, applyDescriptionFromMarkdown(s, vTask, stored)) + assert.Equal(t, stored, vTask.Description) + }) + + t.Run("edited markdown is converted to html", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + const stored = `Hello world
` + vTask := &models.Task{Description: "Hello **mars**"} + + require.NoError(t, applyDescriptionFromMarkdown(s, vTask, stored)) + assert.Equal(t, "Hello mars
", vTask.Description) + }) + + t.Run("mention is rebuilt from markdown", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + vTask := &models.Task{Description: "ping @user1"} + + require.NoError(t, applyDescriptionFromMarkdown(s, vTask, "")) + assert.Contains(t, vTask.Description, `was here
")) + assert.Empty(t, vTask.Description) + }) +} diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 60a151e2d..e59761328 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -28,6 +28,7 @@ import ( "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/richtext" user2 "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/web" "github.com/samedi/caldav-go/data" @@ -364,6 +365,14 @@ func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) ( return nil, errs.ForbiddenError } + // Inbound CalDAV descriptions are markdown; store them as canonical HTML. + if err := applyDescriptionFromMarkdown(s, vTask, ""); err != nil { + log.Errorf("[CALDAV] Failed to convert description in CreateResource: %v", err) + _ = s.Rollback() + events.CleanupPending(s) + return nil, err + } + // Create the task err = vTask.Create(s, vcls.user) if err != nil { @@ -408,6 +417,23 @@ func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) ( return &r, nil } +// applyDescriptionFromMarkdown converts a task's inbound CalDAV description +// (markdown) to canonical HTML, rebuilding @mentions. Unchanged markdown keeps the +// stored HTML verbatim, so a no-op read-modify-write doesn't churn it or move Updated. +func applyDescriptionFromMarkdown(s *xorm.Session, vTask *models.Task, storedHTML string) error { + if !richtext.Changed(storedHTML, vTask.Description) { + vTask.Description = storedHTML + return nil + } + + htmlDesc, err := richtext.MarkdownToHTMLWithMentions(s, vTask.Description) + if err != nil { + return err + } + vTask.Description = htmlDesc + return nil +} + // UpdateResource updates a resource func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (*data.Resource, error) { @@ -443,6 +469,14 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) ( return nil, errs.ForbiddenError } + // Inbound markdown → canonical HTML, kept verbatim when unchanged. + if err := applyDescriptionFromMarkdown(s, vTask, vcls.task.Description); err != nil { + log.Errorf("[CALDAV] Failed to convert description in UpdateResource: %v", err) + _ = s.Rollback() + events.CleanupPending(s) + return nil, err + } + // Update the task err = vTask.Update(s, vcls.user) if err != nil {