summaryrefslogtreecommitdiff
path: root/internal/cli/callayer.go
blob: db59b134eb4571b829c3a4a99b5fa7b479fe5f76 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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.<lang>   e.g. name.en, name.pl, name.la
#   reading.<part>   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.<lang> = 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
// <CalendarsDir>/<name>.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 <CalendarsDir>/<name>.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
}