package cli import ( "fmt" "io" "os" "path/filepath" "sort" "strings" "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/calendar" "github.com/lukaszkasprzak/lectio/internal/config" ) // calLayerTemplate is the scaffold written by --cal-new (%s = the layer name). const calLayerTemplate = `# lectio calendar layer "%[1]s". Add celebrations below, then enable this file # by adding its name to 'use' in config.ini: use = %[1]s # Layers stack over the universal calendar: a [slug] merges over the same slug in # a lower layer (field by field); a new [slug] adds a celebration. # # Fields: # date MM-DD, or easter±N / christmas±N / sunday-after MM-DD # rank solemnity | feast | memorial | optional # class lord | bvm | saint (precedence tiebreak) # colour white | red | green | violet | rose | black # name. e.g. name.en, name.pl, name.la # reading. part = first | psalm | second | acclamation | gospel (a citation) # suppress = true remove a celebration inherited from a lower layer [layer] id = %[1]s name = %[1]s type = diocesan # Example (edit or delete): [st-example] date = 06-15 rank = solemnity class = saint colour = white name.en = Saint Example, Patron reading.gospel = Jn 10:11-16 ` // calDumpHeader precedes a --cal-dump file. %[1]s = the layer name, %[2]s = a // human label for the form (e.g. "Ordinary Form"). const calDumpHeader = `# lectio calendar layer "%[1]s" -- the %[2]s universal calendar, DUMPED and # fully COMMENTED OUT. As shipped it changes nothing. To alter one day, uncomment # its block, edit a field (rank, colour, name, ...), then enable the layer by # adding its name to 'use' in config.ini: use = %[1]s # # A layer OVERLAYS the base calendar field by field, so every day you leave # commented keeps its default. You can also: # * remove a celebration: uncomment its block, add suppress = true # * add one: write a new [your-slug] with date/rank/colour/name.* # * change its readings: add reading.first / reading.psalm / reading.gospel # # Fields: date = MM-DD (or easter+-N / christmas+-N / sunday-after MM-DD) ; # rank = solemnity|feast|memorial|optional ; class = lord|bvm|saint ; # colour = white|red|green|violet|rose|black ; name. = display name. # (reading.* lines are omitted here; add them only to change a day's readings.) # # Re-dump anytime with: lectio --cal-dump %[1]s (add -l trad for the 1962 form). [layer] id = %[1]s name = %[1]s type = diocesan ` var validRanks = map[string]bool{"solemnity": true, "feast": true, "memorial": true, "optional": true} // runCalDump writes the running universal calendar for a form to // /.ini as a fully-commented layer template: every // celebration present, all commented, so uncommenting and editing one day // overrides it. It refuses to overwrite an existing file. formFlag ("old"/"new", // optional) overrides the form implied by cfg. func runCalDump(cfg config.Config, name, formFlag string, stdout, stderr io.Writer) int { sel := cfg.Selection() if formFlag != "" { if formFlag != "old" && formFlag != "new" { fmt.Fprintf(stderr, "lectio: invalid --form %q (want old|new)\n", formFlag) return 2 } sel.Form = formFlag } path, err := layerPath(name) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 2 } if _, err := os.Stat(path); err == nil { fmt.Fprintf(stderr, "lectio: %s already exists (not overwriting)\n", path) return 1 } base := caldata.Base(sel.Form) formLabel := "Ordinary Form" if sel.Form == "old" { formLabel = "Extraordinary Form (1962)" } // Sort by date then slug. MM-DD dates sort chronologically and ahead of the // movable specs (easter+N, christmas-N), which then group alphabetically. slugs := make([]string, 0, len(base.Cels)) for slug := range base.Cels { slugs = append(slugs, slug) } sort.Slice(slugs, func(i, j int) bool { di, dj := base.Cels[slugs[i]].Fields["date"], base.Cels[slugs[j]].Fields["date"] if di != dj { return di < dj } return slugs[i] < slugs[j] }) var b strings.Builder fmt.Fprintf(&b, calDumpHeader, name, formLabel) order := []string{"date", "rank", "class", "colour", "name.en", "name.pl", "name.la"} n := 0 for _, slug := range slugs { f := base.Cels[slug].Fields if f["date"] == "" { continue // skip anything without a date (defensive) } b.WriteByte('\n') fmt.Fprintf(&b, "# [%s]\n", slug) for _, k := range order { if v := f[k]; v != "" { fmt.Fprintf(&b, "# %s = %s\n", k, v) } } n++ } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } fmt.Fprintf(stdout, "dumped %d %s celebrations to %s\n", n, formLabel, path) fmt.Fprintf(stdout, "uncomment a day, edit it, then set use = %s in config.ini\n", name) return 0 } // layerPath resolves /.ini, rejecting a name with path separators. func layerPath(name string) (string, error) { if name == "" || strings.ContainsAny(name, `/\`) { return "", fmt.Errorf("invalid layer name %q", name) } dir, err := config.CalendarsDir() if err != nil { return "", err } return filepath.Join(dir, name+".ini"), nil } // runCalNew scaffolds a new calendar-layer file and exits. It refuses to // overwrite an existing file. func runCalNew(name string, stdout, stderr io.Writer) int { path, err := layerPath(name) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 2 } if _, err := os.Stat(path); err == nil { fmt.Fprintf(stderr, "lectio: %s already exists (not overwriting)\n", path) return 1 } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } if err := os.WriteFile(path, []byte(fmt.Sprintf(calLayerTemplate, name)), 0o644); err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } fmt.Fprintf(stdout, "created %s\n", path) fmt.Fprintf(stdout, "edit it, then set use = %s in config.ini\n", name) return 0 } // runCalCheck validates a calendar-layer file (dates resolve, ranks/colours/ // classes are known) and exits non-zero if it finds problems. func runCalCheck(name string, stdout, stderr io.Writer) int { path, err := layerPath(name) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 2 } layer, err := caldata.LoadLayer(path, name) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } slugs := make([]string, 0, len(layer.Cels)) for s := range layer.Cels { slugs = append(slugs, s) } sort.Strings(slugs) problems := 0 report := func(slug, msg string) { problems++ fmt.Fprintf(stdout, " [%s] %s\n", slug, msg) } for _, slug := range slugs { f := layer.Cels[slug].Fields if f["suppress"] == "true" { continue // a removal needs no other fields } switch d := f["date"]; { case d == "": report(slug, "missing date") case !calendar.ValidDate(calendar.DateSpec(d)): report(slug, fmt.Sprintf("unparseable date %q", d)) } if r := f["rank"]; r != "" && !validRanks[r] { report(slug, fmt.Sprintf("unknown rank %q (want solemnity|feast|memorial|optional)", r)) } if c := f["colour"]; c != "" && calendar.ParseColour(c) == "" { report(slug, fmt.Sprintf("unknown colour %q", c)) } if cl := f["class"]; cl != "" && cl != "lord" && cl != "bvm" && cl != "saint" { report(slug, fmt.Sprintf("unknown class %q (want lord|bvm|saint)", cl)) } } if problems > 0 { fmt.Fprintf(stderr, "lectio: %s: %d problem(s)\n", path, problems) return 1 } fmt.Fprintf(stdout, "ok: %s (%d celebrations)\n", path, len(layer.Cels)) return 0 }