ical.go
⎇
Raw
1// Package ical erzeugt iCalendar-Feeds (RFC 5545) zum Abonnieren in Kalender-Apps.
2package ical
3
4import (
5 "bytes"
6 "strings"
7 "time"
8 "unicode/utf8"
9)
10
11type Event struct {
12 UID string
13 Start, End, Stamp time.Time
14 Summary, Description, Location string
15 Sequence int
16 Cancelled bool
17}
18
19type Calendar struct {
20 Name, ProdID string
21 Events []Event
22}
23
24// Escape für TEXT-Werte (RFC 5545 3.3.11).
25func Escape(s string) string {
26 r := strings.NewReplacer(`\`, `\\`, `;`, `\;`, `,`, `\,`, "\r\n", `\n`, "\n", `\n`, "\r", `\n`)
27 return r.Replace(s)
28}
29
30// fold bricht Zeilen nach höchstens 75 Bytes um, ohne UTF-8-Zeichen zu zerteilen.
31func fold(line string) string {
32 if len(line) <= 75 {
33 return line + "\r\n"
34 }
35 var b strings.Builder
36 limit := 75
37 for len(line) > limit {
38 cut := limit
39 for cut > 0 && !utf8.RuneStart(line[cut]) {
40 cut--
41 }
42 b.WriteString(line[:cut])
43 b.WriteString("\r\n ")
44 line = line[cut:]
45 limit = 74 // Folgezeilen beginnen mit einem Leerzeichen
46 }
47 b.WriteString(line)
48 b.WriteString("\r\n")
49 return b.String()
50}
51
52func utc(t time.Time) string { return t.UTC().Format("20060102T150405Z") }
53
54func (c Calendar) Bytes() []byte {
55 var b bytes.Buffer
56 w := func(s string) { b.WriteString(fold(s)) }
57 w("BEGIN:VCALENDAR")
58 w("VERSION:2.0")
59 w("PRODID:" + c.ProdID)
60 w("CALSCALE:GREGORIAN")
61 w("METHOD:PUBLISH")
62 w("X-WR-CALNAME:" + Escape(c.Name))
63 w("REFRESH-INTERVAL;VALUE=DURATION:PT1H")
64 w("X-PUBLISHED-TTL:PT1H")
65 for _, e := range c.Events {
66 w("BEGIN:VEVENT")
67 w("UID:" + e.UID)
68 w("DTSTAMP:" + utc(e.Stamp))
69 w("DTSTART:" + utc(e.Start))
70 w("DTEND:" + utc(e.End))
71 w("SEQUENCE:" + itoa(e.Sequence))
72 w("SUMMARY:" + Escape(e.Summary))
73 if e.Location != "" {
74 w("LOCATION:" + Escape(e.Location))
75 }
76 if e.Description != "" {
77 w("DESCRIPTION:" + Escape(e.Description))
78 }
79 if e.Cancelled {
80 w("STATUS:CANCELLED")
81 } else {
82 w("STATUS:CONFIRMED")
83 }
84 w("TRANSP:OPAQUE")
85 w("END:VEVENT")
86 }
87 w("END:VCALENDAR")
88 return b.Bytes()
89}
90
91func itoa(n int) string {
92 if n == 0 {
93 return "0"
94 }
95 var d []byte
96 neg := n < 0
97 if neg {
98 n = -n
99 }
100 for n > 0 {
101 d = append([]byte{byte('0' + n%10)}, d...)
102 n /= 10
103 }
104 if neg {
105 d = append([]byte{'-'}, d...)
106 }
107 return string(d)
108}
109