// Command clectio-gen emits the compiled data for clectio, the tiny suckless-C // daily-readings build. It reuses lectio's validated calendar + lectionary // engine to compute, for every day in a year range, the day's name, colour, and // readings -- then writes two artifacts: // // liturgy_
.h : corpus-INDEPENDENT C tables (days, readings, citations, // and verse KEYS as indices) -- shipped, frozen. // verses_.keys : the ordered (book,chapter,verse) keys, one per line -- // shipped, resolved to text at clectio build time (mktext) // against whatever Vulgate-numbered corpus is compiled in. // // So the hard liturgical logic stays here (Go, oracle-validated); the C side is // a dumb lookup + renderer, and swapping the Bible never touches this generator. // // Usage: clectio-gen [-caldir DIR -use NAMES] [-sanctorale DIR] // // By default it uses lectio's embedded, oracle-validated calendar. To bake a // customized calendar into clectio, point it at calendar layers (-caldir with a // comma-separated -use list) and/or a replacement sanctorale (-sanctorale DIR // with of.ini/ef.ini). Only the calendar is taken from those; the book table // stays embedded, so the generator never reads a stale user books.ini. package main import ( "bufio" "flag" "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/liturgy" "github.com/lukaszkasprzak/lectio/internal/readings" ) // colourIndex maps a liturgical colour to the small enum clectio prints. var colourIndex = map[string]int{ "green": 0, "white": 1, "red": 2, "violet": 3, "rose": 4, "black": 5, } // partLabel is the English section label clectio prints, by PartID. func partLabel(id string) string { switch id { case "pierwsze_czytanie": return "First Reading" case "psalm": return "Responsorial Psalm" case "drugie_czytanie": return "Second Reading" case "aklamacja": return "Gospel Acclamation" case "ewangelia", "evangelium": return "Gospel" case "epistola": return "Epistle" } switch { case strings.HasPrefix(id, "lectio"): return "Lesson" case strings.HasPrefix(id, "prophetia"): return "Prophecy" } return "Reading" } // intern assigns a stable index to each distinct string, preserving first-seen // order. type intern struct { idx map[string]int list []string } func newIntern() *intern { return &intern{idx: map[string]int{}} } func (n *intern) get(s string) int { if i, ok := n.idx[s]; ok { return i } i := len(n.list) n.idx[s] = i n.list = append(n.list, s) return i } // reading is one distinct pericope: its part label, citation in each sigla, and // the verse-key indices that make it up. type reading struct { part, citeEN, citeLA int verses []int } // day is one distinct liturgy: its name, colour, and ordered reading indices. type day struct { name, colour int readings []int } func main() { caldir := flag.String("caldir", "", "directory of calendar-layer .ini files to apply (use with -use)") use := flag.String("use", "", "comma-separated layer names to stack over the calendar (needs -caldir)") sanctorale := flag.String("sanctorale", "", "directory with of.ini/ef.ini that REPLACE the embedded sanctorale") flag.Usage = func() { fmt.Fprintln(os.Stderr, "usage: clectio-gen [-caldir DIR -use NAMES] [-sanctorale DIR] ") flag.PrintDefaults() } flag.Parse() args := flag.Args() if len(args) != 4 { flag.Usage() os.Exit(2) } // Hermetic BOOK table: point config at a nonexistent path so the generator // never reads the running user's ~/.config/lectio/books.ini (which could be // stale and silently drop readings). Calendar customization comes ONLY from // the explicit flags below, so the two concerns stay separate. os.Setenv("LECTIO_CONFIG", filepath.Join(os.TempDir(), "clectio-gen-no-such-dir", "config.ini")) if *caldir != "" { config.SetCalendarsDir(*caldir) } if *sanctorale != "" { caldata.SetSanctoraleDir(*sanctorale) } var useList []string if *use != "" { useList = strings.Split(*use, ",") } form := args[0] // "new" (OF) or "old" (EF) lect := "new" tag := "of" if form == "old" { lect, tag = "traditional", "ef" } y0, _ := strconv.Atoi(args[1]) y1, _ := strconv.Atoi(args[2]) outdir := args[3] base := func(sigla string) config.Config { c := config.Default() c.UILanguage = "en" c.Lectionary = lect c.SiglaStyle = sigla c.Use = useList return c } cfgEN, cfgLA := base("english"), base("latin") names := newIntern() parts := newIntern() citesEN := newIntern() citesLA := newIntern() verseKeys := newIntern() // key "Book\tChap\tVerse" -> verse index var readingList []reading readingIdx := map[string]int{} var dayList []day dayIdx := map[string]int{} var cal []int internReading := func(secEN, secLA liturgy.Section) int { keys := bible.LookupKeyed("vul", resolveRef(secEN, form)) if len(keys) == 0 { return -1 } vids := make([]int, len(keys)) for i, k := range keys { vids[i] = verseKeys.get(fmt.Sprintf("%s\t%d\t%d", k.Book, k.Chapter, k.Verse)) } r := reading{ part: parts.get(partLabel(secEN.PartID)), citeEN: citesEN.get(secEN.Citation), citeLA: citesLA.get(secLA.Citation), verses: vids, } key := fmt.Sprintf("%d|%d|%d|%v", r.part, r.citeEN, r.citeLA, vids) if i, ok := readingIdx[key]; ok { return i } i := len(readingList) readingIdx[key] = i readingList = append(readingList, r) return i } start := time.Date(y0, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(y1, 12, 31, 0, 0, 0, 0, time.UTC) for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { ds := d.Format("2006-01-02") secsEN, info, err := readings.Load(cfgEN, readings.Options{Date: ds, All: true}) if err != nil { fmt.Fprintf(os.Stderr, "clectio-gen: %s: %v\n", ds, err) os.Exit(1) } secsLA, _, _ := readings.Load(cfgLA, readings.Options{Date: ds, All: true}) var rids []int for i, secEN := range secsEN { secLA := secEN if i < len(secsLA) { secLA = secsLA[i] } if id := internReading(secEN, secLA); id >= 0 { rids = append(rids, id) } else { fmt.Fprintf(os.Stderr, "clectio-gen: %s DROP part=%s ref=%q cite=%q\n", ds, secEN.PartID, resolveRef(secEN, form), secEN.Citation) } } dy := day{ name: names.get(info.Name), colour: colourIndex[info.Colour], readings: rids, } key := fmt.Sprintf("%d|%d|%v", dy.name, dy.colour, rids) id, ok := dayIdx[key] if !ok { id = len(dayList) dayIdx[key] = id dayList = append(dayList, dy) } cal = append(cal, id) } if err := os.MkdirAll(outdir, 0o755); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } writeKeys(filepath.Join(outdir, "verses_"+tag+".keys"), verseKeys.list) writeHeader(filepath.Join(outdir, "liturgy_"+tag+".h"), tag, y0, names.list, parts.list, citesEN.list, citesLA.list, readingList, dayList, cal) fmt.Printf("clectio-gen %s: %d days, %d distinct days, %d readings, %d verses\n", tag, len(cal), len(dayList), len(readingList), len(verseKeys.list)) } // resolveRef mirrors render.resolveRef: prefer the English-canonical Ref, and // for the Ordinary Form renumber the Psalms to the Vulgate the keys come from. func resolveRef(sec liturgy.Section, form string) string { cit := sec.Ref if cit == "" { cit = sec.Citation } if cit == "" { if c, err := liturgy.ExtractCitation(sec.Heading); err == nil { cit = c } } if cit == "" { return "" } if form != "new" { return cit // EF citations are already Vulgate-numbered } return bible.OFRef(cit, "vulgate") } func writeKeys(path string, keys []string) { f, err := os.Create(path) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer f.Close() w := bufio.NewWriter(f) for _, k := range keys { w.WriteString(k + "\n") } w.Flush() } func cstr(s string) string { r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\t", `\t`) return `"` + r.Replace(s) + `"` } func writeHeader(path, tag string, y0 int, names, parts, citesEN, citesLA []string, readings []reading, days []day, cal []int) { f, err := os.Create(path) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer f.Close() w := bufio.NewWriter(f) defer w.Flush() guard := "LITURGY_" + strings.ToUpper(tag) + "_H" fmt.Fprintf(w, "/* generated by clectio-gen -- do not edit */\n#ifndef %s\n#define %s\n\n", guard, guard) fmt.Fprintf(w, "#define EPOCH_Y %d\n#define EPOCH_M 1\n#define EPOCH_D 1\n#define NDAYS %d\n\n", y0, len(cal)) strArr := func(name string, xs []string) { fmt.Fprintf(w, "static const char *const %s[] = {\n", name) for _, s := range xs { fmt.Fprintf(w, "\t%s,\n", cstr(s)) } fmt.Fprintf(w, "};\n\n") } strArr("names", names) strArr("parts", parts) strArr("cites_en", citesEN) strArr("cites_la", citesLA) // Flat verse-index pool; each reading points at a [voff,voff+vlen) slice. var vpool []int fmt.Fprintf(w, "static const unsigned short vpool[] = {") for _, r := range readings { for _, v := range r.verses { vpool = append(vpool, v) } } for i, v := range vpool { if i%16 == 0 { fmt.Fprintf(w, "\n\t") } fmt.Fprintf(w, "%d,", v) } fmt.Fprintf(w, "\n};\n\n") fmt.Fprintf(w, "typedef struct { unsigned short part, cite, voff, vlen; } Reading;\n") fmt.Fprintf(w, "static const Reading readings[] = {\n") voff := 0 for _, r := range readings { fmt.Fprintf(w, "\t{%d,%d,%d,%d},\n", r.part, r.citeEN, voff, len(r.verses)) voff += len(r.verses) } fmt.Fprintf(w, "};\n\n") // cite_la parallels readings[] by index (same part/verses, latin citation). fmt.Fprintf(w, "static const unsigned short readings_cite_la[] = {") for i, r := range readings { if i%16 == 0 { fmt.Fprintf(w, "\n\t") } fmt.Fprintf(w, "%d,", r.citeLA) } fmt.Fprintf(w, "\n};\n\n") // Flat reading-index pool; each day points at a [roff,roff+rlen) slice. var rpool []int fmt.Fprintf(w, "static const unsigned short rpool[] = {") for _, d := range days { for _, r := range d.readings { rpool = append(rpool, r) } } for i, r := range rpool { if i%16 == 0 { fmt.Fprintf(w, "\n\t") } fmt.Fprintf(w, "%d,", r) } fmt.Fprintf(w, "\n};\n\n") fmt.Fprintf(w, "typedef struct { unsigned short name; unsigned char colour; unsigned short roff, rlen; } Day;\n") fmt.Fprintf(w, "static const Day days[] = {\n") roff := 0 for _, d := range days { fmt.Fprintf(w, "\t{%d,%d,%d,%d},\n", d.name, d.colour, roff, len(d.readings)) roff += len(d.readings) } fmt.Fprintf(w, "};\n\n") fmt.Fprintf(w, "static const unsigned short cal[NDAYS] = {") for i, c := range cal { if i%16 == 0 { fmt.Fprintf(w, "\n\t") } fmt.Fprintf(w, "%d,", c) } fmt.Fprintf(w, "\n};\n\n#endif\n") }