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 "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.
30type 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.
36type 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.
48var 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.
53var 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.
71type 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.
77func (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.
97func (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.
196func 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.
203func (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.
222const 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.
250func (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
261func init() { registerTemplate(startedBody) }
262