first commit
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// Package ical implements a minimal RFC 5545 iCalendar parser/builder — just
|
||||
// VEVENT with the fields CalDAV needs: UID, SUMMARY, DTSTART, DTEND,
|
||||
// DESCRIPTION, LOCATION. Not full RFC 5545 (no VTODO/VJOURNAL/VALARM, no
|
||||
// RRULE recurrence) — enough for real calendar clients to create, fetch, and
|
||||
// list single events, with recurrence and other component types as natural
|
||||
// next additions once client compatibility testing calls for them.
|
||||
package ical
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const icalTimeLayout = "20060102T150405Z"
|
||||
|
||||
type Event struct {
|
||||
UID string
|
||||
Summary string
|
||||
Description string
|
||||
Location string
|
||||
DTStart time.Time
|
||||
DTEnd time.Time
|
||||
}
|
||||
|
||||
// Parse reads a VCALENDAR containing one VEVENT.
|
||||
func Parse(data string) (*Event, error) {
|
||||
lines := unfold(data)
|
||||
e := &Event{}
|
||||
inEvent := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
upper := strings.ToUpper(line)
|
||||
switch {
|
||||
case upper == "BEGIN:VEVENT":
|
||||
inEvent = true
|
||||
continue
|
||||
case upper == "END:VEVENT":
|
||||
inEvent = false
|
||||
continue
|
||||
}
|
||||
if !inEvent {
|
||||
continue
|
||||
}
|
||||
|
||||
name, value, found := splitProperty(line)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(name) {
|
||||
case "UID":
|
||||
e.UID = value
|
||||
case "SUMMARY":
|
||||
e.Summary = unescape(value)
|
||||
case "DESCRIPTION":
|
||||
e.Description = unescape(value)
|
||||
case "LOCATION":
|
||||
e.Location = unescape(value)
|
||||
case "DTSTART":
|
||||
if t, err := time.Parse(icalTimeLayout, value); err == nil {
|
||||
e.DTStart = t
|
||||
}
|
||||
case "DTEND":
|
||||
if t, err := time.Parse(icalTimeLayout, value); err == nil {
|
||||
e.DTEnd = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.UID == "" {
|
||||
return nil, fmt.Errorf("ical missing required UID property")
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Build renders an Event back into a full VCALENDAR/VEVENT block, CRLF line
|
||||
// endings per spec.
|
||||
func (e *Event) Build() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("BEGIN:VCALENDAR\r\n")
|
||||
b.WriteString("VERSION:2.0\r\n")
|
||||
b.WriteString("PRODID:-//GoMail//CalDAV//EN\r\n")
|
||||
b.WriteString("BEGIN:VEVENT\r\n")
|
||||
b.WriteString("UID:" + e.UID + "\r\n")
|
||||
if !e.DTStart.IsZero() {
|
||||
b.WriteString("DTSTART:" + e.DTStart.UTC().Format(icalTimeLayout) + "\r\n")
|
||||
}
|
||||
if !e.DTEnd.IsZero() {
|
||||
b.WriteString("DTEND:" + e.DTEnd.UTC().Format(icalTimeLayout) + "\r\n")
|
||||
}
|
||||
if e.Summary != "" {
|
||||
b.WriteString("SUMMARY:" + escape(e.Summary) + "\r\n")
|
||||
}
|
||||
if e.Description != "" {
|
||||
b.WriteString("DESCRIPTION:" + escape(e.Description) + "\r\n")
|
||||
}
|
||||
if e.Location != "" {
|
||||
b.WriteString("LOCATION:" + escape(e.Location) + "\r\n")
|
||||
}
|
||||
b.WriteString("END:VEVENT\r\n")
|
||||
b.WriteString("END:VCALENDAR\r\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func splitProperty(line string) (name, value string, found bool) {
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx == -1 {
|
||||
return "", "", false
|
||||
}
|
||||
namePart := line[:colonIdx]
|
||||
value = line[colonIdx+1:]
|
||||
if semiIdx := strings.Index(namePart, ";"); semiIdx != -1 {
|
||||
namePart = namePart[:semiIdx]
|
||||
}
|
||||
return namePart, value, true
|
||||
}
|
||||
|
||||
// unfold reverses RFC 5545 §3.1 line folding, same rule as vCard's.
|
||||
func unfold(data string) []string {
|
||||
raw := strings.Split(strings.ReplaceAll(data, "\r\n", "\n"), "\n")
|
||||
var out []string
|
||||
for _, line := range raw {
|
||||
if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') && len(out) > 0 {
|
||||
out[len(out)-1] += line[1:]
|
||||
} else {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func escape(s string) string {
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
s = strings.ReplaceAll(s, ",", "\\,")
|
||||
s = strings.ReplaceAll(s, ";", "\\;")
|
||||
s = strings.ReplaceAll(s, "\n", "\\n")
|
||||
return s
|
||||
}
|
||||
|
||||
func unescape(s string) string {
|
||||
s = strings.ReplaceAll(s, "\\n", "\n")
|
||||
s = strings.ReplaceAll(s, "\\,", ",")
|
||||
s = strings.ReplaceAll(s, "\\;", ";")
|
||||
s = strings.ReplaceAll(s, "\\\\", "\\")
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user