aboutsummaryrefslogtreecommitdiff
path: root/internal/ini/ini.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 12:12:19 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 12:12:19 +0200
commitb4982d22be0c79949046c9d957354209996acb2e (patch)
tree7608b1c303a2f977a7b4d309072172a900305739 /internal/ini/ini.go
parentc5d4388215713324a6cfdebc20aaa5ef594f0174 (diff)
downloadlectio-b4982d22be0c79949046c9d957354209996acb2e.tar.gz
lectio-b4982d22be0c79949046c9d957354209996acb2e.zip
feat(ini): dependency-free INI reader for config and calendar data
Diffstat (limited to 'internal/ini/ini.go')
-rw-r--r--internal/ini/ini.go75
1 files changed, 75 insertions, 0 deletions
diff --git a/internal/ini/ini.go b/internal/ini/ini.go
new file mode 100644
index 0000000..b4fc955
--- /dev/null
+++ b/internal/ini/ini.go
@@ -0,0 +1,75 @@
+// Package ini is a tiny dependency-free reader for the boring INI files lectio
+// uses for config and calendar data: [sections] (including "a/b" subsections),
+// "key = value" pairs (keys may be dotted, e.g. name.pl), and "#"/";" comments.
+// It preserves order and never interprets values beyond trimming.
+package ini
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+type Pair struct{ Key, Val string }
+
+type Section struct {
+ Name string
+ Pairs []Pair
+}
+
+// Parse reads INI bytes into ordered sections. Pairs before the first [section]
+// go into a leading section with an empty Name.
+func Parse(data []byte) ([]Section, error) {
+ secs := []Section{{Name: ""}}
+ sc := bufio.NewScanner(bytes.NewReader(data))
+ sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
+ line := 0
+ for sc.Scan() {
+ line++
+ raw := stripComment(sc.Text())
+ s := strings.TrimSpace(raw)
+ if s == "" {
+ continue
+ }
+ if strings.HasPrefix(s, "[") {
+ if !strings.HasSuffix(s, "]") {
+ return nil, fmt.Errorf("ini: line %d: unclosed section header %q", line, s)
+ }
+ name := strings.TrimSpace(s[1 : len(s)-1])
+ secs = append(secs, Section{Name: name})
+ continue
+ }
+ eq := strings.IndexByte(s, '=')
+ if eq < 0 {
+ return nil, fmt.Errorf("ini: line %d: expected key = value, got %q", line, s)
+ }
+ key := strings.TrimSpace(s[:eq])
+ val := strings.TrimSpace(s[eq+1:])
+ cur := &secs[len(secs)-1]
+ cur.Pairs = append(cur.Pairs, Pair{key, val})
+ }
+ return secs, sc.Err()
+}
+
+// stripComment removes a trailing "#" or ";" comment (our values never contain
+// an unescaped # or ;).
+func stripComment(line string) string {
+ for i := 0; i < len(line); i++ {
+ if line[i] == '#' || line[i] == ';' {
+ return line[:i]
+ }
+ }
+ return line
+}
+
+// List splits a comma-separated value, trimming spaces and dropping empties.
+func List(val string) []string {
+ var out []string
+ for _, p := range strings.Split(val, ",") {
+ if t := strings.TrimSpace(p); t != "" {
+ out = append(out, t)
+ }
+ }
+ return out
+}