rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package gallery
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/fs"
7 "sort"
8 "strings"
9
10 "amadan.net/rastrillo/fiesta/example"
11)
12
13// The design system's explanations: a sentence on each family, each
14// example and some states, and the two rules about cards and columns
15// that head the primitives. They are read from examples/prose.json,
16// which hack/seed/run-prose.sh writes from rastrillo's gallery, with
17// the strings that could not carry replaced by Fiesta's own after copy
18// review. The gallery shows them beside the markup; nothing else in
19// Fiesta reads them.
20
21// proseFile is where the explanations live in the source tree.
22const proseFile = "examples/prose.json"
23
24// prose is the whole file.
25type prose struct {
26 Families map[string]familyProse `json:"families"`
27 Examples map[string]exampleProse `json:"examples"`
28 Pages pagesProse `json:"pages"`
29}
30
31// familyProse is a family's heading and the sentence under it.
32type familyProse struct {
33 Title string `json:"title"`
34 Blurb string `json:"blurb"`
35}
36
37// exampleProse is an example's sentence and, by state id, the note
38// under a state's label and, where a state has one, the warning
39// callout after it. A state with nothing to say has no entry; an
40// entry that is "" is a slot waiting for copy review.
41type exampleProse struct {
42 Blurb string `json:"blurb"`
43 States map[string]string `json:"states,omitempty"`
44 Warnings map[string]warning `json:"warnings,omitempty"`
45}
46
47// warning is a callout under one state's note: its title and body.
48type warning struct {
49 Title string `json:"title"`
50 Body string `json:"body"`
51}
52
53// pagesProse is the prose that belongs to a page rather than to an
54// example: the idiom rules, which head the primitives family.
55type pagesProse struct {
56 Idioms []idiomRule `json:"idioms"`
57}
58
59// idiomRule is one rule, quoted on the index at the head of the family
60// that holds the idiom it names, which is the primitives.
61type idiomRule struct {
62 Idiom string `json:"idiom"`
63 Title string `json:"title"`
64 Body string `json:"body"`
65}
66
67// loadProse reads the explanations from src.
68func loadProse(src fs.FS) (*prose, error) {
69 b, err := fs.ReadFile(src, proseFile)
70 if err != nil {
71 return nil, err
72 }
73 var p prose
74 if err := json.Unmarshal(b, &p); err != nil {
75 return nil, fmt.Errorf("%s: %w", proseFile, err)
76 }
77 return &p, nil
78}
79
80// strays lists every slot in the prose that names no family, example,
81// state or idiom in the manifest: the ids a typo would leave behind,
82// each with what is wrong with it.
83func (p *prose) strays(m *example.Manifest) []string {
84 families := map[string]bool{}
85 for _, e := range m.Examples {
86 families[e.Family] = true
87 }
88 var out []string
89 for key := range p.Families {
90 if !families[key] {
91 out = append(out, "families."+key+": no example is in that family")
92 }
93 }
94 for name, ex := range p.Examples {
95 e, ok := m.Entry(name)
96 if !ok {
97 out = append(out, "examples."+name+": no such example")
98 continue
99 }
100 for id := range ex.States {
101 if !hasState(e, id) {
102 out = append(out, "examples."+name+".states."+id+": no such state")
103 }
104 }
105 for id := range ex.Warnings {
106 if !hasState(e, id) {
107 out = append(out, "examples."+name+".warnings."+id+": no such state")
108 }
109 }
110 }
111 for _, rule := range p.Pages.Idioms {
112 if _, _, ok := m.Idiom(rule.Idiom); !ok {
113 out = append(out, "pages.idioms."+rule.Idiom+": no such idiom")
114 }
115 }
116 sort.Strings(out)
117 return out
118}
119
120// hasState says whether e has a state with id.
121func hasState(e example.Entry, id string) bool {
122 for _, st := range e.States {
123 if st.ID == id {
124 return true
125 }
126 }
127 return false
128}
129
130// english is the English the prose holds in the slot id names, as each
131// names the slots, or "" for a slot it has not got. Resolved from the
132// id's parts rather than by walking each: a render asks for a title
133// per page per rail entry, and the walk was most of a render.
134func (p *prose) english(id string) string {
135 parts := strings.Split(id, ".")
136 switch {
137 case len(parts) == 3 && parts[0] == "families":
138 f := p.Families[parts[1]]
139 return map[string]string{"title": f.Title, "blurb": f.Blurb}[parts[2]]
140 case len(parts) == 3 && parts[0] == "examples" && parts[2] == "blurb":
141 return p.Examples[parts[1]].Blurb
142 case len(parts) == 4 && parts[0] == "examples" && parts[2] == "states":
143 return p.Examples[parts[1]].States[parts[3]]
144 case len(parts) == 5 && parts[0] == "examples" && parts[2] == "warnings":
145 w := p.Examples[parts[1]].Warnings[parts[3]]
146 return map[string]string{"title": w.Title, "body": w.Body}[parts[4]]
147 case len(parts) == 4 && parts[0] == "pages" && parts[1] == "idioms":
148 for _, r := range p.Pages.Idioms {
149 if r.Idiom == parts[2] {
150 return map[string]string{"title": r.Title, "body": r.Body}[parts[3]]
151 }
152 }
153 }
154 return ""
155}
156
157// each calls fn with every string in the prose and the slot it sits
158// in, in a fixed order, so a check over the words reads all of them.
159func (p *prose) each(fn func(id, text string)) {
160 keys := make([]string, 0, len(p.Families))
161 for k := range p.Families {
162 keys = append(keys, k)
163 }
164 sort.Strings(keys)
165 for _, k := range keys {
166 fn("families."+k+".title", p.Families[k].Title)
167 fn("families."+k+".blurb", p.Families[k].Blurb)
168 }
169 names := make([]string, 0, len(p.Examples))
170 for n := range p.Examples {
171 names = append(names, n)
172 }
173 sort.Strings(names)
174 for _, n := range names {
175 ex := p.Examples[n]
176 fn("examples."+n+".blurb", ex.Blurb)
177 ids := make([]string, 0, len(ex.States))
178 for id := range ex.States {
179 ids = append(ids, id)
180 }
181 sort.Strings(ids)
182 for _, id := range ids {
183 fn("examples."+n+".states."+id, ex.States[id])
184 }
185 warned := make([]string, 0, len(ex.Warnings))
186 for id := range ex.Warnings {
187 warned = append(warned, id)
188 }
189 sort.Strings(warned)
190 for _, id := range warned {
191 fn("examples."+n+".warnings."+id+".title", ex.Warnings[id].Title)
192 fn("examples."+n+".warnings."+id+".body", ex.Warnings[id].Body)
193 }
194 }
195 for _, rule := range p.Pages.Idioms {
196 fn("pages.idioms."+rule.Idiom+".title", rule.Title)
197 fn("pages.idioms."+rule.Idiom+".body", rule.Body)
198 }
199}
200