| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "html/template" |
| 6 | "sort" |
| 7 | "strings" |
| 8 | |
| 9 | "amadan.net/rastrillo/fiesta" |
| 10 | "amadan.net/rastrillo/fiesta/internal/build" |
| 11 | "amadan.net/rastrillo/fiesta/internal/codeview" |
| 12 | ) |
| 13 | |
| 14 | // Getting started: what ships and what it weighs, read off the release |
| 15 | // build while the page renders. Every weight on the page is weigh() of |
| 16 | // the bytes build.Dist returns for fiesta.Version; no figure is written |
| 17 | // in copy.go or here, so a page about file sizes stays true after |
| 18 | // someone edits a file. Each row links its file where the site serves |
| 19 | // it, under ReleaseBase, marked data-gallery-site: the one link form |
| 20 | // internal/www resolves through its redirect to the newest release. |
| 21 | // |
| 22 | // Rastrillo's page lists what a scaffolded app is handed; this one |
| 23 | // lists the release, so the rows and the total are Fiesta's own, while |
| 24 | // the view types, the rail reader and the weight line keep Rastrillo's |
| 25 | // names. |
| 26 | |
| 27 | // assetView is one shipped file: what it is called, where the site |
| 28 | // serves it, what it weighs (the finished line in the page's language) |
| 29 | // and what it is for. |
| 30 | type assetView struct{ ID, Name, Href, Weight, Blurb string } |
| 31 | |
| 32 | // assetsView is the page's data: the rows in page order, the total a |
| 33 | // page that links fiesta.css and every script pays, and the two code |
| 34 | // blocks, highlighted: the one link, and the two-file form for a site |
| 35 | // serving its own copy. |
| 36 | type assetsView struct { |
| 37 | List []assetView |
| 38 | Total string |
| 39 | Link, Own template.HTML |
| 40 | } |
| 41 | |
| 42 | // scriptOrder is the order the scripts are listed in: fiesta.js first |
| 43 | // and calendar.js last, as the page says to load them, the rest as the |
| 44 | // scripts paragraph names them. That is the order for the deferred |
| 45 | // scripts; transition.js's row says it loads before the stylesheets. |
| 46 | // A script in the release and not here fails the render by name, so a |
| 47 | // new script cannot ship off this page. |
| 48 | var scriptOrder = []string{"fiesta.js", "busy.js", "light.js", "anchor.js", "shell.js", "transition.js", "copy.js", "select.js", "datetime.js", "calendar.js"} |
| 49 | |
| 50 | // blurbs is the copy id of each shipped file whose sentence has no |
| 51 | // placeholder. The one-file themes, the themes and the strings take |
| 52 | // theirs with a theme or a language written in; see buildAssets. |
| 53 | var blurbs = map[string]string{ |
| 54 | "tokens.css": "gallery.started.file.tokens", |
| 55 | "shell.css": "gallery.started.file.shell-css", |
| 56 | "fiesta.js": "gallery.started.file.fiesta-js", |
| 57 | "busy.js": "gallery.started.file.busy-js", |
| 58 | "light.js": "gallery.started.file.light-js", |
| 59 | "anchor.js": "gallery.started.file.anchor-js", |
| 60 | "shell.js": "gallery.started.file.shell-js", |
| 61 | "transition.js": "gallery.started.file.transition-js", |
| 62 | "copy.js": "gallery.started.file.copy-js", |
| 63 | "select.js": "gallery.started.file.select-js", |
| 64 | "datetime.js": "gallery.started.file.datetime-js", |
| 65 | "calendar.js": "gallery.started.file.calendar-js", |
| 66 | "icons.svg": "gallery.started.file.sprite", |
| 67 | } |
| 68 | |
| 69 | // release is the release build of one site's source, reduced to what |
| 70 | // the page needs: every file's weight, by name. |
| 71 | type release map[string]weight |
| 72 | |
| 73 | // weighRelease builds the release from s's source and weighs every |
| 74 | // file in it. newSite calls it once and keeps the result on the site: |
| 75 | // the rail asks assetNav for the rows on every page of the tree, and a |
| 76 | // release build and forty gzips a page would be most of the render. |
| 77 | func (s *site) weighRelease() (release, error) { |
| 78 | dist, err := build.Dist(s.src, fiesta.Version) |
| 79 | if err != nil { |
| 80 | return nil, err |
| 81 | } |
| 82 | rel := make(release, len(dist)) |
| 83 | for name, body := range dist { |
| 84 | rel[name] = weigh(body) |
| 85 | } |
| 86 | return rel, nil |
| 87 | } |
| 88 | |
| 89 | // buildAssets reads the release build for p's page: the rows in the |
| 90 | // order the spec lists them (the one-file themes, fiesta.css first; |
| 91 | // tokens.css; each theme; shell.css; the scripts; the sprite; the |
| 92 | // strings, one per language), each weighed from the bytes the release |
| 93 | // holds. Every file the release ships is a row, or one of the files the |
| 94 | // page leaves out by design (LICENSE, NOTICE, SHA256SUMS, the per-icon |
| 95 | // SVGs the Icons page draws, the JSON the strings scripts wrap), or an |
| 96 | // error naming it. |
| 97 | func (s *site) buildAssets(p page) (assetsView, error) { |
| 98 | rel := s.weighed |
| 99 | var out assetsView |
| 100 | listed := map[string]bool{} |
| 101 | add := func(name, blurb string) error { |
| 102 | w, ok := rel[name] |
| 103 | if !ok { |
| 104 | return fmt.Errorf("gallery: Getting started lists %s and the release has no such file", name) |
| 105 | } |
| 106 | listed[name] = true |
| 107 | out.List = append(out.List, assetView{ |
| 108 | ID: anchorID("asset", name), |
| 109 | Name: name, |
| 110 | Href: ReleaseBase + name, |
| 111 | Weight: s.weightIn(p.Locale, w), |
| 112 | Blurb: blurb, |
| 113 | }) |
| 114 | return nil |
| 115 | } |
| 116 | // The one-file themes. The default theme's is fiesta.css: the one |
| 117 | // theme with no fiesta-<theme>.css of its own. |
| 118 | themes := fiesta.ThemeNames() |
| 119 | def := "" |
| 120 | for _, theme := range themes { |
| 121 | if _, ok := rel["fiesta-"+theme+".css"]; !ok { |
| 122 | if def != "" { |
| 123 | return assetsView{}, fmt.Errorf("gallery: both %s and %s lack a one-file theme of their own", def, theme) |
| 124 | } |
| 125 | def = theme |
| 126 | } |
| 127 | } |
| 128 | if def == "" { |
| 129 | return assetsView{}, fmt.Errorf("gallery: no theme is fiesta.css") |
| 130 | } |
| 131 | if err := add("fiesta.css", s.say(p.Locale, "gallery.started.file.bundle", "theme", def)); err != nil { |
| 132 | return assetsView{}, err |
| 133 | } |
| 134 | for _, theme := range themes { |
| 135 | if theme == def { |
| 136 | continue |
| 137 | } |
| 138 | if err := add("fiesta-"+theme+".css", s.say(p.Locale, "gallery.started.file.bundle", "theme", theme)); err != nil { |
| 139 | return assetsView{}, err |
| 140 | } |
| 141 | } |
| 142 | if err := add("tokens.css", s.say(p.Locale, blurbs["tokens.css"])); err != nil { |
| 143 | return assetsView{}, err |
| 144 | } |
| 145 | for _, theme := range themes { |
| 146 | if err := add(theme+".css", s.say(p.Locale, "gallery.started.file.theme", "theme", theme)); err != nil { |
| 147 | return assetsView{}, err |
| 148 | } |
| 149 | } |
| 150 | for _, name := range append([]string{"shell.css"}, scriptOrder...) { |
| 151 | if err := add(name, s.say(p.Locale, blurbs[name])); err != nil { |
| 152 | return assetsView{}, err |
| 153 | } |
| 154 | } |
| 155 | if err := add("icons.svg", s.say(p.Locale, blurbs["icons.svg"])); err != nil { |
| 156 | return assetsView{}, err |
| 157 | } |
| 158 | var langs []string |
| 159 | for name := range rel { |
| 160 | if strings.HasPrefix(name, "i18n/") && strings.HasSuffix(name, ".js") { |
| 161 | langs = append(langs, strings.TrimSuffix(strings.TrimPrefix(name, "i18n/"), ".js")) |
| 162 | } |
| 163 | } |
| 164 | sort.Strings(langs) |
| 165 | for _, lang := range langs { |
| 166 | // Each language named in itself, as the language menu names it. |
| 167 | if err := add("i18n/"+lang+".js", s.say(p.Locale, "gallery.started.file.strings", "language", s.say(lang, "gallery.locale.name"))); err != nil { |
| 168 | return assetsView{}, err |
| 169 | } |
| 170 | } |
| 171 | for name := range rel { |
| 172 | switch { |
| 173 | case listed[name], name == "LICENSE", name == "NOTICE", name == "SHA256SUMS", |
| 174 | strings.HasPrefix(name, "icons/"), |
| 175 | strings.HasPrefix(name, "i18n/") && strings.HasSuffix(name, ".json"): |
| 176 | continue |
| 177 | } |
| 178 | return assetsView{}, fmt.Errorf("gallery: the release ships %s and Getting started has no row for it", name) |
| 179 | } |
| 180 | |
| 181 | // The total: fiesta.css and every script, each weighed on its own, |
| 182 | // the way a visitor downloads them. |
| 183 | raw, gz := 0, 0 |
| 184 | for _, name := range append([]string{"fiesta.css"}, scriptOrder...) { |
| 185 | raw, gz = raw+rel[name].Raw, gz+rel[name].Gzip |
| 186 | } |
| 187 | out.Total = s.say(p.Locale, "gallery.started.total", "raw", kibUnit(p.Locale, raw), "gzip", kibUnit(p.Locale, gz)) |
| 188 | out.Link = codeview.Highlight(`<link rel="stylesheet" href="` + ReleaseBase + `fiesta.css">`) |
| 189 | out.Own = codeview.Highlight(`<link rel="stylesheet" href="tokens.css">` + "\n" + `<link rel="stylesheet" href="` + p.Theme + `.css">`) |
| 190 | return out, nil |
| 191 | } |
| 192 | |
| 193 | // kibUnit is kib with the unit after it, for a sentence that stands on |
| 194 | // its own rather than under a note saying the unit. KiB is the unit's |
| 195 | // own symbol, written the same in every language. |
| 196 | func kibUnit(locale string, n int) string { return kib(locale, n) + " KiB" } |
| 197 | |
| 198 | // assetNav is one rail entry per shipped file, in the order the page |
| 199 | // lists them, derived from the same view the page renders from, so a |
| 200 | // file joins both lists on the day it ships, or neither. A release the |
| 201 | // view cannot be built from is reported by startedBody, which fails |
| 202 | // the render; the empty rail returned here is never written. |
| 203 | func (s *site) assetNav(p page) []navItem { |
| 204 | view, err := s.buildAssets(p) |
| 205 | if err != nil { |
| 206 | return nil |
| 207 | } |
| 208 | file := s.href(p.Theme, p.Locale, s.fileOf("getting-started")) |
| 209 | var items []navItem |
| 210 | for _, a := range view.List { |
| 211 | items = append(items, navItem{Label: a.Name, Href: file + "#" + a.ID, Code: true}) |
| 212 | } |
| 213 | return items |
| 214 | } |
| 215 | |
| 216 | // startedBody is the page a reader meets before any of the vocabulary: |
| 217 | // what ships, what each file is for and what it weighs, then the one |
| 218 | // link, the two-file form for a site serving its own copy, and how an |
| 219 | // upgrade reaches a page. The code blocks are translate="no": the |
| 220 | // address in the first is a URL the site's link gate resolves like any |
| 221 | // other, and neither is prose. |
| 222 | const startedBody = `{{define "ds-body-getting-started"}}<p class="ds-lead">{{say "gallery.started.lead"}}</p> |
| 223 | |
| 224 | <h2 class="ds-sub">{{say "gallery.started.stylesheets"}}</h2> |
| 225 | <p class="ds-lead">{{say "gallery.started.stylesheets.body"}}</p> |
| 226 | |
| 227 | <h2 class="ds-sub">{{say "gallery.started.scripts"}}</h2> |
| 228 | <p class="ds-lead">{{say "gallery.started.scripts.body"}}</p> |
| 229 | |
| 230 | <h2 class="ds-sub">{{say "gallery.started.weights"}}</h2> |
| 231 | <p class="ds-note">{{say "gallery.started.weights.note"}}</p> |
| 232 | <ul class="ds-files">{{range .Assets.List}} |
| 233 | <li id="{{.ID}}" data-ds-anchor><a class="rst-mono" data-gallery-site href="{{.Href}}">{{.Name}}</a><span>{{.Blurb}}</span><span class="rst-mono">{{.Weight}}</span></li>{{end}} |
| 234 | </ul> |
| 235 | <p class="ds-lead">{{.Assets.Total}}</p> |
| 236 | |
| 237 | <h2 class="ds-sub">{{say "gallery.started.link.heading"}}</h2> |
| 238 | <p class="ds-lead">{{say "gallery.started.link.body"}}</p> |
| 239 | <pre class="ds-src rst-mono" tabindex="0"><code translate="no">{{.Assets.Link}}</code></pre> |
| 240 | |
| 241 | <h2 class="ds-sub">{{say "gallery.started.own.heading"}}</h2> |
| 242 | <p class="ds-lead">{{say "gallery.started.own.body"}}</p> |
| 243 | <pre class="ds-src rst-mono" tabindex="0"><code translate="no">{{.Assets.Own}}</code></pre> |
| 244 | |
| 245 | <h2 class="ds-sub">{{say "gallery.started.upgrading"}}</h2> |
| 246 | <p class="ds-lead">{{say "gallery.started.upgrading.body"}}</p> |
| 247 | {{end}}` |
| 248 | |
| 249 | // startedBody renders the Getting started page's body for p. |
| 250 | func (s *site) startedBody(p page) (template.HTML, error) { |
| 251 | assets, err := s.buildAssets(p) |
| 252 | if err != nil { |
| 253 | return "", err |
| 254 | } |
| 255 | return s.body(p.Locale, "ds-body-getting-started", struct { |
| 256 | page |
| 257 | Assets assetsView |
| 258 | }{p, assets}) |
| 259 | } |
| 260 | |
| 261 | func init() { registerTemplate(startedBody) } |
| 262 | |