// 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 full-line // "#"/";" comments. Comments are FULL-LINE only (a line whose first non-blank // character is # or ;); values are kept verbatim so scripture citations may // contain ";" and "#". It preserves order and never interprets values beyond // trimming surrounding whitespace. 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++ s := strings.TrimSpace(sc.Text()) if s == "" || strings.HasPrefix(s, "#") || strings.HasPrefix(s, ";") { continue // blank or full-line comment } 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:]) // verbatim (may contain ';' or '#') cur := &secs[len(secs)-1] cur.Pairs = append(cur.Pairs, Pair{key, val}) } return secs, sc.Err() } // 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 }