package gallery import ( "encoding/json" "fmt" "io/fs" "sort" "strings" "amadan.net/rastrillo/fiesta/example" ) // The design system's explanations: a sentence on each family, each // example and some states, and the two rules about cards and columns // that head the primitives. They are read from examples/prose.json, // which hack/seed/run-prose.sh writes from rastrillo's gallery, with // the strings that could not carry replaced by Fiesta's own after copy // review. The gallery shows them beside the markup; nothing else in // Fiesta reads them. // proseFile is where the explanations live in the source tree. const proseFile = "examples/prose.json" // prose is the whole file. type prose struct { Families map[string]familyProse `json:"families"` Examples map[string]exampleProse `json:"examples"` Pages pagesProse `json:"pages"` } // familyProse is a family's heading and the sentence under it. type familyProse struct { Title string `json:"title"` Blurb string `json:"blurb"` } // exampleProse is an example's sentence and, by state id, the note // under a state's label and, where a state has one, the warning // callout after it. A state with nothing to say has no entry; an // entry that is "" is a slot waiting for copy review. type exampleProse struct { Blurb string `json:"blurb"` States map[string]string `json:"states,omitempty"` Warnings map[string]warning `json:"warnings,omitempty"` } // warning is a callout under one state's note: its title and body. type warning struct { Title string `json:"title"` Body string `json:"body"` } // pagesProse is the prose that belongs to a page rather than to an // example: the idiom rules, which head the primitives family. type pagesProse struct { Idioms []idiomRule `json:"idioms"` } // idiomRule is one rule, quoted on the index at the head of the family // that holds the idiom it names, which is the primitives. type idiomRule struct { Idiom string `json:"idiom"` Title string `json:"title"` Body string `json:"body"` } // loadProse reads the explanations from src. func loadProse(src fs.FS) (*prose, error) { b, err := fs.ReadFile(src, proseFile) if err != nil { return nil, err } var p prose if err := json.Unmarshal(b, &p); err != nil { return nil, fmt.Errorf("%s: %w", proseFile, err) } return &p, nil } // strays lists every slot in the prose that names no family, example, // state or idiom in the manifest: the ids a typo would leave behind, // each with what is wrong with it. func (p *prose) strays(m *example.Manifest) []string { families := map[string]bool{} for _, e := range m.Examples { families[e.Family] = true } var out []string for key := range p.Families { if !families[key] { out = append(out, "families."+key+": no example is in that family") } } for name, ex := range p.Examples { e, ok := m.Entry(name) if !ok { out = append(out, "examples."+name+": no such example") continue } for id := range ex.States { if !hasState(e, id) { out = append(out, "examples."+name+".states."+id+": no such state") } } for id := range ex.Warnings { if !hasState(e, id) { out = append(out, "examples."+name+".warnings."+id+": no such state") } } } for _, rule := range p.Pages.Idioms { if _, _, ok := m.Idiom(rule.Idiom); !ok { out = append(out, "pages.idioms."+rule.Idiom+": no such idiom") } } sort.Strings(out) return out } // hasState says whether e has a state with id. func hasState(e example.Entry, id string) bool { for _, st := range e.States { if st.ID == id { return true } } return false } // english is the English the prose holds in the slot id names, as each // names the slots, or "" for a slot it has not got. Resolved from the // id's parts rather than by walking each: a render asks for a title // per page per rail entry, and the walk was most of a render. func (p *prose) english(id string) string { parts := strings.Split(id, ".") switch { case len(parts) == 3 && parts[0] == "families": f := p.Families[parts[1]] return map[string]string{"title": f.Title, "blurb": f.Blurb}[parts[2]] case len(parts) == 3 && parts[0] == "examples" && parts[2] == "blurb": return p.Examples[parts[1]].Blurb case len(parts) == 4 && parts[0] == "examples" && parts[2] == "states": return p.Examples[parts[1]].States[parts[3]] case len(parts) == 5 && parts[0] == "examples" && parts[2] == "warnings": w := p.Examples[parts[1]].Warnings[parts[3]] return map[string]string{"title": w.Title, "body": w.Body}[parts[4]] case len(parts) == 4 && parts[0] == "pages" && parts[1] == "idioms": for _, r := range p.Pages.Idioms { if r.Idiom == parts[2] { return map[string]string{"title": r.Title, "body": r.Body}[parts[3]] } } } return "" } // each calls fn with every string in the prose and the slot it sits // in, in a fixed order, so a check over the words reads all of them. func (p *prose) each(fn func(id, text string)) { keys := make([]string, 0, len(p.Families)) for k := range p.Families { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fn("families."+k+".title", p.Families[k].Title) fn("families."+k+".blurb", p.Families[k].Blurb) } names := make([]string, 0, len(p.Examples)) for n := range p.Examples { names = append(names, n) } sort.Strings(names) for _, n := range names { ex := p.Examples[n] fn("examples."+n+".blurb", ex.Blurb) ids := make([]string, 0, len(ex.States)) for id := range ex.States { ids = append(ids, id) } sort.Strings(ids) for _, id := range ids { fn("examples."+n+".states."+id, ex.States[id]) } warned := make([]string, 0, len(ex.Warnings)) for id := range ex.Warnings { warned = append(warned, id) } sort.Strings(warned) for _, id := range warned { fn("examples."+n+".warnings."+id+".title", ex.Warnings[id].Title) fn("examples."+n+".warnings."+id+".body", ex.Warnings[id].Body) } } for _, rule := range p.Pages.Idioms { fn("pages.idioms."+rule.Idiom+".title", rule.Title) fn("pages.idioms."+rule.Idiom+".body", rule.Body) } }