106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
package classes
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// ICS writes an iCalendar (RFC 5545) stream: CRLF line endings, content
|
|
// lines folded at 75 octets without splitting a UTF-8 sequence, and TEXT
|
|
// values escaped. The zero value is ready to use.
|
|
type ICS struct {
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
// icsLineOctets is the longest content line RFC 5545 allows, not counting
|
|
// the CRLF.
|
|
const icsLineOctets = 75
|
|
|
|
// Begin writes BEGIN:<component>.
|
|
func (w *ICS) Begin(component string) { w.Line("BEGIN:" + component) }
|
|
|
|
// End writes END:<component>.
|
|
func (w *ICS) End(component string) { w.Line("END:" + component) }
|
|
|
|
// Prop writes name:value with the value as it is (a value of a type that
|
|
// needs no escaping, such as a date, a duration or a URI).
|
|
func (w *ICS) Prop(name, value string) { w.Line(name + ":" + value) }
|
|
|
|
// Text writes name:value with the value escaped as TEXT.
|
|
func (w *ICS) Text(name, value string) { w.Line(name + ":" + EscapeText(value)) }
|
|
|
|
// UTC writes name:<t in UTC>, such as DTSTART:20261003T060000Z.
|
|
func (w *ICS) UTC(name string, t time.Time) { w.Prop(name, FormatUTC(t)) }
|
|
|
|
// Date writes name;VALUE=DATE:<YYYYMMDD> for an all-day value.
|
|
func (w *ICS) Date(name string, t time.Time) { w.Prop(name+";VALUE=DATE", t.Format("20060102")) }
|
|
|
|
// Line writes one content line, folded.
|
|
func (w *ICS) Line(line string) {
|
|
line = strings.NewReplacer("\r", "", "\n", "").Replace(line)
|
|
w.buf.WriteString(FoldLine(line))
|
|
w.buf.WriteString("\r\n")
|
|
}
|
|
|
|
// Bytes returns the stream written so far.
|
|
func (w *ICS) Bytes() []byte { return w.buf.Bytes() }
|
|
|
|
// FormatUTC formats t as an RFC 5545 UTC date-time (20261003T060000Z).
|
|
func FormatUTC(t time.Time) string { return t.UTC().Format("20060102T150405Z") }
|
|
|
|
// EscapeText escapes a TEXT value: backslash, semicolon, comma and line
|
|
// breaks (as \n). A lone CR is dropped.
|
|
func EscapeText(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s))
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
for _, r := range s {
|
|
switch r {
|
|
case '\\':
|
|
b.WriteString(`\\`)
|
|
case ';':
|
|
b.WriteString(`\;`)
|
|
case ',':
|
|
b.WriteString(`\,`)
|
|
case '\n':
|
|
b.WriteString(`\n`)
|
|
case '\r':
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FoldLine folds a content line (without its CRLF) so that no physical line
|
|
// is longer than 75 octets. Continuation lines start with one space, which
|
|
// counts towards their 75 octets; a multi-octet UTF-8 character is never
|
|
// split.
|
|
func FoldLine(line string) string {
|
|
if len(line) <= icsLineOctets {
|
|
return line
|
|
}
|
|
var b strings.Builder
|
|
n := 0
|
|
for i := 0; i < len(line); {
|
|
size := runeBytes(line[i:])
|
|
if n+size > icsLineOctets {
|
|
b.WriteString("\r\n ")
|
|
n = 1
|
|
}
|
|
b.WriteString(line[i : i+size])
|
|
n += size
|
|
i += size
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// runeBytes is the length in bytes of the first character of s, also for an
|
|
// invalid sequence (one byte).
|
|
func runeBytes(s string) int {
|
|
_, size := utf8.DecodeRuneInString(s)
|
|
return size
|
|
}
|