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 ` var validRanks = map[string]bool{"solemnity": true, "feast": true, "memorial": true, "optional": true} // 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 }