| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "html/template" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // The page kinds: one page per section, each at its own URL under |
| 10 | // <theme>/<locale>/. The table pageKinds returns is the whole of that |
| 11 | // seam: a row in it is a page in the tree, a section of the rail, a |
| 12 | // row on the phone index, a step in the previous/next sequence, a |
| 13 | // route off the Overview and a file TestTreeShapeIsComplete expects, |
| 14 | // with nothing else to remember. The families come after the three |
| 15 | // fixed rows, read off the manifest, so a family added to examples/ |
| 16 | // is a page the day it lands. |
| 17 | |
| 18 | // ReleaseBase is where the site serves the newest 1.x release's files. |
| 19 | // A link to a shipped file starts with it and carries data-gallery-site; |
| 20 | // internal/www holds it to its own origin. |
| 21 | const ReleaseBase = "https://fiesta.rastrillo.org/1/" |
| 22 | |
| 23 | // familyOrder is the order the families are read in: the gallery's |
| 24 | // own, as the spec gives it, and not the order the manifest happens to |
| 25 | // name them in. It is the family rows of pageKinds, the sections of |
| 26 | // the rail and the walk of the sequence, which ends on the last shell. |
| 27 | // A family in the manifest and not here fails the render by name. |
| 28 | var familyOrder = []string{"list-screen", "display", "form", "choices-and-actions", "date-and-time", "route", "primitives", "formats", "screens", "shells"} |
| 29 | |
| 30 | // inFamilyOrder says whether familyOrder places key. |
| 31 | func inFamilyOrder(key string) bool { |
| 32 | for _, k := range familyOrder { |
| 33 | if k == key { |
| 34 | return true |
| 35 | } |
| 36 | } |
| 37 | return false |
| 38 | } |
| 39 | |
| 40 | // pageKind is one row of the table. Title and Blurb are ids: a |
| 41 | // gallery.* id into text, or a prose slot id (families.<key>.title); |
| 42 | // textOf resolves either. Blurb is the one sentence the Overview says |
| 43 | // when it routes a reader to the page, and is empty on the Overview, |
| 44 | // which never routes to itself. Nav reads the page's rail entries off |
| 45 | // the finished page, nil where it anchors nothing. Body renders the |
| 46 | // page's own markup. |
| 47 | type pageKind struct { |
| 48 | Kind, File, Title, Blurb string |
| 49 | Nav func(p page) []navItem |
| 50 | Body func(p page) (template.HTML, error) |
| 51 | } |
| 52 | |
| 53 | // pageKinds is the table, in page order: the Overview, Getting |
| 54 | // started, Tokens, Icons, then one row per family in the gallery's |
| 55 | // family order. |
| 56 | func (s *site) pageKinds() []pageKind { |
| 57 | kinds := []pageKind{ |
| 58 | {Kind: "overview", File: "index.html", Title: "gallery.kind.overview", Body: s.overviewBody}, |
| 59 | {Kind: "getting-started", File: "getting-started.html", Title: "gallery.kind.getting-started", Blurb: "gallery.routes.getting-started", Nav: s.assetNav, Body: s.startedBody}, |
| 60 | {Kind: "tokens", File: "tokens.html", Title: "gallery.kind.tokens", Blurb: "gallery.route.tokens", Nav: s.tokenNav, Body: s.tokensBody}, |
| 61 | {Kind: "icons", File: "icons.html", Title: "gallery.kind.icons", Blurb: "gallery.routes.icons", Nav: s.iconNav, Body: s.iconsBody}, |
| 62 | } |
| 63 | for _, f := range s.families { |
| 64 | kinds = append(kinds, pageKind{ |
| 65 | Kind: f.Key, |
| 66 | File: f.Key + ".html", |
| 67 | Title: "families." + f.Key + ".title", |
| 68 | Blurb: "families." + f.Key + ".blurb", |
| 69 | Nav: s.familyNav(f.Key), |
| 70 | Body: s.familyBody, |
| 71 | }) |
| 72 | } |
| 73 | return kinds |
| 74 | } |
| 75 | |
| 76 | // fileOf is one page kind's file. It panics on a kind that is not in |
| 77 | // the table, since every caller passes a Kind read out of the table. |
| 78 | func (s *site) fileOf(kind string) string { |
| 79 | for _, pk := range s.pageKinds() { |
| 80 | if pk.Kind == kind { |
| 81 | return pk.File |
| 82 | } |
| 83 | } |
| 84 | panic("gallery: no page kind " + kind) |
| 85 | } |
| 86 | |
| 87 | // href is the address of one file of one theme and locale directory. |
| 88 | // Every link the renderer writes into the tree is built here, so the |
| 89 | // mount is spelled once. |
| 90 | func (s *site) href(theme, locale, file string) string { |
| 91 | return s.url(theme + "/" + locale + "/" + file) |
| 92 | } |
| 93 | |
| 94 | // titleOf is one page kind's title in locale. |
| 95 | func (s *site) titleOf(locale string, pk pageKind) string { return s.textOf(locale, pk.Title) } |
| 96 | |
| 97 | // blurbOf is one page kind's route blurb in locale, or "" where it has |
| 98 | // none. |
| 99 | func (s *site) blurbOf(locale string, pk pageKind) string { |
| 100 | if pk.Blurb == "" { |
| 101 | return "" |
| 102 | } |
| 103 | return s.textOf(locale, pk.Blurb, "total", fmt.Sprint(len(s.icons))) |
| 104 | } |
| 105 | |
| 106 | // sequence is the order the pages are read in: the table's rows, and |
| 107 | // inside each family its examples in manifest order after the family's |
| 108 | // own page. Each step is the page's address and the label the |
| 109 | // previous/next pair names it by: a kind's title, an example's name. |
| 110 | func (s *site) sequence(theme, locale string) []link { |
| 111 | var out []link |
| 112 | for _, pk := range s.pageKinds() { |
| 113 | out = append(out, link{Href: s.href(theme, locale, pk.File), Label: s.titleOf(locale, pk)}) |
| 114 | for _, f := range s.families { |
| 115 | if f.Key != pk.Kind { |
| 116 | continue |
| 117 | } |
| 118 | for _, e := range f.Examples { |
| 119 | out = append(out, link{Href: s.href(theme, locale, e.Name+".html"), Label: e.Name}) |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | return out |
| 124 | } |
| 125 | |
| 126 | // steps is the pair of links at the foot of one page: the page before |
| 127 | // it and the page after it in the sequence. Either is nil at the ends. |
| 128 | func (s *site) steps(theme, locale, file string) (prev, next *link) { |
| 129 | seq := s.sequence(theme, locale) |
| 130 | at := -1 |
| 131 | for i, step := range seq { |
| 132 | if step.Href == s.href(theme, locale, file) { |
| 133 | at = i |
| 134 | break |
| 135 | } |
| 136 | } |
| 137 | if at < 0 { |
| 138 | panic("gallery: " + file + " is not in the sequence") |
| 139 | } |
| 140 | step := func(i int, id string) *link { |
| 141 | if i < 0 || i >= len(seq) { |
| 142 | return nil |
| 143 | } |
| 144 | return &link{Href: seq[i].Href, Label: s.say(locale, id, "page", seq[i].Label)} |
| 145 | } |
| 146 | return step(at-1, "gallery.step.prev"), step(at+1, "gallery.step.next") |
| 147 | } |
| 148 | |
| 149 | // route is one entry in the Overview's route list: another page of the |
| 150 | // gallery, its name, and the one sentence saying what is on it. |
| 151 | type route struct{ Label, Blurb, Href string } |
| 152 | |
| 153 | // routes is the Overview's way into the rest of the gallery: one per |
| 154 | // page kind that is not kind, in table order, each with its row's |
| 155 | // blurb. |
| 156 | func (s *site) routes(theme, locale, kind string) []route { |
| 157 | var out []route |
| 158 | for _, pk := range s.pageKinds() { |
| 159 | if pk.Kind == kind { |
| 160 | continue |
| 161 | } |
| 162 | out = append(out, route{Label: s.titleOf(locale, pk), Blurb: s.blurbOf(locale, pk), Href: s.href(theme, locale, pk.File)}) |
| 163 | } |
| 164 | return out |
| 165 | } |
| 166 | |
| 167 | // anchorID is the id one anchorable thing on a page wears, and the |
| 168 | // fragment the rail links it by: built from the English name, so the |
| 169 | // twelve translations of a page carry the same fragments, and prefixed |
| 170 | // by kind, so a token group and an icon of the same name are two ids. |
| 171 | func anchorID(kind, name string) string { return kind + "-" + slug(name) } |
| 172 | |
| 173 | // slug lowercases and hyphenates: ASCII letters and digits survive, |
| 174 | // everything else becomes one hyphen, and the ends are trimmed. The |
| 175 | // inputs are English headings and identifiers already in this |
| 176 | // alphabet, so there is no transliteration to do. |
| 177 | func slug(s string) string { |
| 178 | var b strings.Builder |
| 179 | dash := false |
| 180 | for _, r := range strings.ToLower(s) { |
| 181 | switch { |
| 182 | case r >= 'a' && r <= 'z', r >= '0' && r <= '9': |
| 183 | b.WriteRune(r) |
| 184 | dash = false |
| 185 | default: |
| 186 | if b.Len() > 0 && !dash { |
| 187 | b.WriteByte('-') |
| 188 | dash = true |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | return strings.TrimSuffix(b.String(), "-") |
| 193 | } |
| 194 | |