diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 12:12:19 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 12:12:19 +0200 |
| commit | b4982d22be0c79949046c9d957354209996acb2e (patch) | |
| tree | 7608b1c303a2f977a7b4d309072172a900305739 | |
| parent | c5d4388215713324a6cfdebc20aaa5ef594f0174 (diff) | |
| download | lectio-b4982d22be0c79949046c9d957354209996acb2e.tar.gz lectio-b4982d22be0c79949046c9d957354209996acb2e.zip | |
feat(ini): dependency-free INI reader for config and calendar data
| -rw-r--r-- | internal/ini/ini.go | 75 | ||||
| -rw-r--r-- | internal/ini/ini_test.go | 48 |
2 files changed, 123 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 +} diff --git a/internal/ini/ini_test.go b/internal/ini/ini_test.go new file mode 100644 index 0000000..d454388 --- /dev/null +++ b/internal/ini/ini_test.go @@ -0,0 +1,48 @@ +package ini + +import "testing" + +func TestParseSectionsAndPairs(t *testing.T) { + in := []byte(` +# a comment +lectionary = new ; trailing comment + +[assumption] +date = 08-15 +name.pl = Wniebowzięcie ; unicode ok +rank= solemnity + +[assumption/vigil] +reading.gospel = Łk 11,27-28 +`) + secs, err := Parse(in) + if err != nil { + t.Fatal(err) + } + // top-level pairs land in a section with empty name, first. + if secs[0].Name != "" || len(secs[0].Pairs) != 1 || secs[0].Pairs[0] != (Pair{"lectionary", "new"}) { + t.Fatalf("top-level = %+v", secs[0]) + } + if secs[1].Name != "assumption" || secs[1].Pairs[1] != (Pair{"name.pl", "Wniebowzięcie"}) { + t.Fatalf("assumption = %+v", secs[1]) + } + if secs[1].Pairs[2] != (Pair{"rank", "solemnity"}) { + t.Fatalf("no-space key = %+v", secs[1].Pairs[2]) + } + if secs[2].Name != "assumption/vigil" { + t.Fatalf("subsection = %q", secs[2].Name) + } +} + +func TestList(t *testing.T) { + got := List("bt, wuj ,, vul") + want := []string{"bt", "wuj", "vul"} + if len(got) != len(want) { + t.Fatalf("List = %v", got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("List[%d] = %q want %q", i, got[i], want[i]) + } + } +} |
