feat(caldav): serialize task descriptions as markdown
CalDAV clients render DESCRIPTION as plain text, so convert the stored HTML to markdown when serializing VTODOs. On the near-impossible conversion error, log it and keep the stored value.
This commit is contained in:
parent
9015bad65c
commit
a728e50796
|
|
@ -21,7 +21,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
"code.vikunja.io/api/pkg/models"
|
"code.vikunja.io/api/pkg/models"
|
||||||
|
"code.vikunja.io/api/pkg/richtext"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -179,8 +181,18 @@ DURATION:PT` + formatDuration(t.Duration)
|
||||||
DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
|
DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
|
||||||
}
|
}
|
||||||
if t.Description != "" {
|
if t.Description != "" {
|
||||||
caldavtodos += `
|
// CalDAV clients show plain text, so emit markdown. On the near-impossible
|
||||||
DESCRIPTION:` + escapeICalText(t.Description)
|
// conversion error, log it and keep the stored value (GetContent can't
|
||||||
|
// return an error) rather than drop the description.
|
||||||
|
description, err := richtext.HTMLToMarkdown(t.Description)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[CALDAV] Failed to convert description to markdown for task %q: %v", t.UID, err)
|
||||||
|
description = t.Description
|
||||||
|
}
|
||||||
|
if description != "" {
|
||||||
|
caldavtodos += `
|
||||||
|
DESCRIPTION:` + escapeICalText(description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if t.Completed.Unix() > 0 {
|
if t.Completed.Unix() > 0 {
|
||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,11 @@ func TestParseTodos(t *testing.T) {
|
||||||
},
|
},
|
||||||
todos: []*Todo{
|
todos: []*Todo{
|
||||||
{
|
{
|
||||||
Summary: "Todo #1",
|
Summary: "Todo #1",
|
||||||
Description: `Lorem Ipsum
|
Description: `<p>Lorem Ipsum</p><p>Dolor sit amet</p>`,
|
||||||
Dolor sit amet`,
|
UID: "randommduid",
|
||||||
UID: "randommduid",
|
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
Color: "affffe",
|
||||||
Color: "affffe",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -73,7 +72,7 @@ X-APPLE-CALENDAR-COLOR:#affffeFF
|
||||||
X-OUTLOOK-COLOR:#affffeFF
|
X-OUTLOOK-COLOR:#affffeFF
|
||||||
X-FUNAMBOL-COLOR:#affffeFF
|
X-FUNAMBOL-COLOR:#affffeFF
|
||||||
COLOR:#affffeFF
|
COLOR:#affffeFF
|
||||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
DESCRIPTION:Lorem Ipsum\n\nDolor sit amet
|
||||||
LAST-MODIFIED:00010101T000000Z
|
LAST-MODIFIED:00010101T000000Z
|
||||||
END:VTODO
|
END:VTODO
|
||||||
END:VCALENDAR`,
|
END:VCALENDAR`,
|
||||||
|
|
@ -438,6 +437,33 @@ END:VCALENDAR`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTodosRichTextDescription(t *testing.T) {
|
||||||
|
cfg := &Config{Name: "test", ProdID: "Vikunja"}
|
||||||
|
ts := time.Unix(1543626724, 0).In(config.GetTimeZone())
|
||||||
|
|
||||||
|
t.Run("rich html serializes as markdown", func(t *testing.T) {
|
||||||
|
out := ParseTodos(cfg, []*Todo{{
|
||||||
|
Summary: "Todo",
|
||||||
|
UID: "uid",
|
||||||
|
Timestamp: ts,
|
||||||
|
Description: `<p>Hello <strong>bold</strong> and <mention-user data-id="user1" data-label="User One">@User One</mention-user></p>` +
|
||||||
|
`<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>done</p></div></li></ul>`,
|
||||||
|
}})
|
||||||
|
// iCal escapes the markdown's newlines as "\n".
|
||||||
|
assert.Contains(t, out, `DESCRIPTION:Hello **bold** and @user1\n\n- [x] done`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty html omits the description line", func(t *testing.T) {
|
||||||
|
out := ParseTodos(cfg, []*Todo{{Summary: "Todo", UID: "uid", Timestamp: ts, Description: "<p></p>"}})
|
||||||
|
assert.NotContains(t, out, "DESCRIPTION:")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("plain text description is unaffected", func(t *testing.T) {
|
||||||
|
out := ParseTodos(cfg, []*Todo{{Summary: "Todo", UID: "uid", Timestamp: ts, Description: "just plain text"}})
|
||||||
|
assert.Contains(t, out, "DESCRIPTION:just plain text")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetCaldavColor(t *testing.T) {
|
func TestGetCaldavColor(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue