| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "amadan.net/rastrillo/fiesta/example" |
| 8 | ) |
| 9 | |
| 10 | // An example's page and its frame documents. frameFiles names the |
| 11 | // documents and example writes them, under exactly those names, which |
| 12 | // is how the tree gate, the frame count and the accessibility scan |
| 13 | // learn of a new document the day a later task adds one here. |
| 14 | |
| 15 | // frameFiles is the paths, under the theme and locale directory, of |
| 16 | // one example's frame documents: one per state, and for a grouped |
| 17 | // example the row document its one widget frames as well. The |
| 18 | // per-state documents of a grouped example are still written, because |
| 19 | // the accessibility gate scans one state at a time. |
| 20 | func (s *site) frameFiles(e example.Entry) []string { |
| 21 | var out []string |
| 22 | for _, st := range e.States { |
| 23 | out = append(out, "frames/"+e.Name+"/"+st.ID+".html") |
| 24 | } |
| 25 | if grouped[e.Name] { |
| 26 | out = append(out, "frames/"+e.Name+"/row.html") |
| 27 | } |
| 28 | return out |
| 29 | } |
| 30 | |
| 31 | // stateView is one state on an example's page. |
| 32 | type stateView struct { |
| 33 | ID string |
| 34 | Label string |
| 35 | Note string |
| 36 | Warning *warning |
| 37 | Preview previewView |
| 38 | Routes string |
| 39 | } |
| 40 | |
| 41 | // groupNote is one state under a grouped widget: its id, its label on |
| 42 | // a line of its own, and its note where the prose has one. The two |
| 43 | // are separate elements rather than one string joined with ": ", |
| 44 | // because that joiner is copy no review saw, and an ASCII colon is |
| 45 | // wrong punctuation in ja, zh-Hans and yue. |
| 46 | type groupNote struct { |
| 47 | ID, State, Note string |
| 48 | } |
| 49 | |
| 50 | // exampleBody is one example's page: its family as a kicker, its own |
| 51 | // sentence, the two sentences every example page says, then every |
| 52 | // state as a frame of its own document with the note on the state |
| 53 | // under its label, the warning where the prose has one, the widget, |
| 54 | // and the routes it stands for; or, for a grouped example, one widget |
| 55 | // and a block per state under it. The three primitives that are also |
| 56 | // pages of their own end with the link to that page, and a layout's |
| 57 | // page with the button that opens the shell's document, each in a tab |
| 58 | // of its own. |
| 59 | const exampleBody = `{{define "ds-body-example"}}<p class="ds-kicker"><b>{{.Family.Title}}</b> {{.Family.Blurb}}</p> |
| 60 | {{if .Blurb}}<p rst-page-header-sub>{{.Blurb}}</p> |
| 61 | {{end}}<p class="ds-lead">{{say "gallery.example.live"}} {{say "gallery.example.html"}}</p> |
| 62 | {{with .Group}}<section class="ds-sample ds-sample--rows" id="{{$.Entry.Name}}" data-ds-anchor> |
| 63 | {{template "ds-view" .}} |
| 64 | {{range $.Notes}}<div class="ds-group-note" id="{{.ID}}" data-ds-anchor data-gallery-state="{{$.Entry.Name}}/{{.ID}}"><b class="ds-note__state">{{.State}}</b> |
| 65 | {{if .Note}}<p class="ds-note">{{.Note}}</p> |
| 66 | {{end}}</div> |
| 67 | {{end}}</section> |
| 68 | {{else}}{{range .States}}<section class="ds-sample" id="{{.ID}}" data-ds-anchor data-gallery-state="{{$.Entry.Name}}/{{.ID}}"> |
| 69 | <h2 class="ds-state">{{.Label}}</h2> |
| 70 | {{if .Note}}<p class="ds-note">{{.Note}}</p> |
| 71 | {{end}}{{with .Warning}}<div rst-callout rst-tone="warning"><span rst-callout-ic aria-hidden="true">{{icon "alert-triangle"}}</span><div rst-callout-body><strong>{{.Title}}</strong><p>{{.Body}}</p></div></div> |
| 72 | {{end}}{{template "ds-view" .Preview}} |
| 73 | {{if .Routes}}<p class="ds-routes-note">{{.Routes}}</p> |
| 74 | {{end}}</section> |
| 75 | {{end}}{{end}}{{with .Live}}<p class="ds-note"><a data-gallery-nav href="{{.Href}}" target="_blank" rel="noopener">{{.Label}}<span class="rst-sr-only"> ({{say "gallery.newtab"}})</span></a></p> |
| 76 | {{end}}{{if .Shell}}<p><a rst-btn data-gallery-nav href="{{.ShellHref}}" target="_blank" rel="noopener">{{say "gallery.shell.open" "shell" .Shell}}<span class="rst-sr-only"> ({{say "gallery.newtab"}})</span></a></p> |
| 77 | {{end}}{{end}}` |
| 78 | |
| 79 | // demoIdiom is one primitive that is also a page of the tree: the |
| 80 | // words its link wears, by copy id, and the address it goes to. |
| 81 | type demoIdiom struct { |
| 82 | Label string |
| 83 | Href func(s *site, theme, locale string) string |
| 84 | } |
| 85 | |
| 86 | // demoIdioms are the three primitives whose preview is not the last |
| 87 | // word on them. The two shells are whole page frames, better seen at |
| 88 | // the size of a window than of a paragraph, so each links its layout's |
| 89 | // document; the modal's whole claim is that it is a URL, so the |
| 90 | // demonstration is not complete until you have been to it. Rastrillo's |
| 91 | // three, under Rastrillo's name. |
| 92 | var demoIdioms = map[string]demoIdiom{ |
| 93 | "shell-topbar": {"gallery.primitives.topbar.live", func(s *site, theme, locale string) string { return s.layoutHref(theme, locale, "layout-topbar") }}, |
| 94 | "shell-sidebar": {"gallery.primitives.sidebar.live", func(s *site, theme, locale string) string { return s.layoutHref(theme, locale, "layout-sidebar") }}, |
| 95 | "modal": {"gallery.primitives.modal.live", (*site).modalHref}, |
| 96 | } |
| 97 | |
| 98 | // layoutHref is a layout example's document opened whole: its default |
| 99 | // state's frame document. |
| 100 | func (s *site) layoutHref(theme, locale, name string) string { |
| 101 | state, ok := s.m.Default(name) |
| 102 | if !ok { |
| 103 | // The layouts are frozen in examples/ and named in code; a |
| 104 | // manifest without one is a programming error, as a kind |
| 105 | // fileOf does not know is. |
| 106 | panic("gallery: the manifest has no " + name + " example") |
| 107 | } |
| 108 | return s.href(theme, locale, "frames/"+name+"/"+state+".html") |
| 109 | } |
| 110 | |
| 111 | // shellOf is the shell a layout example documents, by the name its |
| 112 | // open button says: column, topbar, sidebar, console or stage. The |
| 113 | // -page members of the sidebar and console pairs are the same shell's |
| 114 | // content page and get no button; a component is no shell at all. |
| 115 | func shellOf(e example.Entry) string { |
| 116 | if !e.Document || strings.HasSuffix(e.Name, "-page") { |
| 117 | return "" |
| 118 | } |
| 119 | return strings.TrimPrefix(e.Name, "layout-") |
| 120 | } |
| 121 | |
| 122 | // example renders one example's page body and its frame documents, |
| 123 | // keyed by the paths frameFiles names. |
| 124 | func (s *site) example(p page, e example.Entry) (body []byte, frames map[string][]byte, err error) { |
| 125 | ex := s.prose.Examples[e.Name] |
| 126 | frames = map[string][]byte{} |
| 127 | data := struct { |
| 128 | page |
| 129 | Family family |
| 130 | Entry example.Entry |
| 131 | Blurb string |
| 132 | States []stateView |
| 133 | Group *previewView |
| 134 | Notes []groupNote |
| 135 | Shell string |
| 136 | ShellHref string |
| 137 | Live *link |
| 138 | }{page: p, Entry: e, Blurb: s.proseIn(p.Locale, "examples."+e.Name+".blurb", ex.Blurb)} |
| 139 | f, _ := s.familyByKey(e.Family) |
| 140 | data.Family = s.familyIn(p.Locale, f) |
| 141 | for _, st := range e.States { |
| 142 | fr, code, err := s.localSample(p.Theme, p.Locale, e, st) |
| 143 | if err != nil { |
| 144 | return nil, nil, fmt.Errorf("%s/%s: %w", e.Name, st.ID, err) |
| 145 | } |
| 146 | rel := "frames/" + e.Name + "/" + st.ID + ".html" |
| 147 | frames[rel] = []byte(fr) |
| 148 | note := s.proseIn(p.Locale, "examples."+e.Name+".states."+st.ID, ex.States[st.ID]) |
| 149 | if grouped[e.Name] { |
| 150 | data.Notes = append(data.Notes, groupNote{ID: st.ID, State: s.label(p.Locale, e, st), Note: note}) |
| 151 | continue |
| 152 | } |
| 153 | view, err := s.preview(p, e, st, s.href(p.Theme, p.Locale, rel), code) |
| 154 | if err != nil { |
| 155 | return nil, nil, fmt.Errorf("%s/%s: %w", e.Name, st.ID, err) |
| 156 | } |
| 157 | sv := stateView{ID: st.ID, Label: s.label(p.Locale, e, st), Note: note, Preview: view} |
| 158 | if len(st.SampleURLs) > 0 { |
| 159 | sv.Routes = s.say(p.Locale, "gallery.routes", "routes", strings.Join(st.SampleURLs, ", ")) |
| 160 | } |
| 161 | if w, ok := ex.Warnings[st.ID]; ok { |
| 162 | slot := "examples." + e.Name + ".warnings." + st.ID |
| 163 | sv.Warning = &warning{ |
| 164 | Title: s.proseIn(p.Locale, slot+".title", w.Title), |
| 165 | Body: s.proseIn(p.Locale, slot+".body", w.Body), |
| 166 | } |
| 167 | } |
| 168 | data.States = append(data.States, sv) |
| 169 | } |
| 170 | if grouped[e.Name] { |
| 171 | view, rowDoc, err := s.groupedPreview(p, e) |
| 172 | if err != nil { |
| 173 | return nil, nil, err |
| 174 | } |
| 175 | fr, err := s.frame(rowDoc, p.Theme, p.Locale, e, "row") |
| 176 | if err != nil { |
| 177 | return nil, nil, fmt.Errorf("%s/row: %w", e.Name, err) |
| 178 | } |
| 179 | frames["frames/"+e.Name+"/row.html"] = []byte(fr) |
| 180 | data.Group = &view |
| 181 | } |
| 182 | if d, ok := demoIdioms[e.Name]; ok { |
| 183 | data.Live = &link{Href: d.Href(s, p.Theme, p.Locale), Label: s.say(p.Locale, d.Label)} |
| 184 | } |
| 185 | if data.Shell = shellOf(e); data.Shell != "" { |
| 186 | data.ShellHref = s.href(p.Theme, p.Locale, "frames/"+e.Name+"/"+e.Default+".html") |
| 187 | } |
| 188 | out, err := s.body(p.Locale, "ds-body-example", data) |
| 189 | if err != nil { |
| 190 | return nil, nil, err |
| 191 | } |
| 192 | return []byte(out), frames, nil |
| 193 | } |
| 194 | |
| 195 | func init() { registerTemplate(exampleBody) } |
| 196 | |