package gallery
import (
"fmt"
"html/template"
"sort"
"strings"
"amadan.net/rastrillo/fiesta"
"amadan.net/rastrillo/fiesta/internal/build"
"amadan.net/rastrillo/fiesta/internal/codeview"
)
// Getting started: what ships and what it weighs, read off the release
// build while the page renders. Every weight on the page is weigh() of
// the bytes build.Dist returns for fiesta.Version; no figure is written
// in copy.go or here, so a page about file sizes stays true after
// someone edits a file. Each row links its file where the site serves
// it, under ReleaseBase, marked data-gallery-site: the one link form
// internal/www resolves through its redirect to the newest release.
//
// Rastrillo's page lists what a scaffolded app is handed; this one
// lists the release, so the rows and the total are Fiesta's own, while
// the view types, the rail reader and the weight line keep Rastrillo's
// names.
// assetView is one shipped file: what it is called, where the site
// serves it, what it weighs (the finished line in the page's language)
// and what it is for.
type assetView struct{ ID, Name, Href, Weight, Blurb string }
// assetsView is the page's data: the rows in page order, the total a
// page that links fiesta.css and every script pays, and the two code
// blocks, highlighted: the one link, and the two-file form for a site
// serving its own copy.
type assetsView struct {
List []assetView
Total string
Link, Own template.HTML
}
// scriptOrder is the order the scripts are listed in: fiesta.js first
// and calendar.js last, as the page says to load them, the rest as the
// scripts paragraph names them. That is the order for the deferred
// scripts; transition.js's row says it loads before the stylesheets.
// A script in the release and not here fails the render by name, so a
// new script cannot ship off this page.
var scriptOrder = []string{"fiesta.js", "busy.js", "light.js", "anchor.js", "shell.js", "transition.js", "copy.js", "select.js", "datetime.js", "calendar.js"}
// blurbs is the copy id of each shipped file whose sentence has no
// placeholder. The one-file themes, the themes and the strings take
// theirs with a theme or a language written in; see buildAssets.
var blurbs = map[string]string{
"tokens.css": "gallery.started.file.tokens",
"shell.css": "gallery.started.file.shell-css",
"fiesta.js": "gallery.started.file.fiesta-js",
"busy.js": "gallery.started.file.busy-js",
"light.js": "gallery.started.file.light-js",
"anchor.js": "gallery.started.file.anchor-js",
"shell.js": "gallery.started.file.shell-js",
"transition.js": "gallery.started.file.transition-js",
"copy.js": "gallery.started.file.copy-js",
"select.js": "gallery.started.file.select-js",
"datetime.js": "gallery.started.file.datetime-js",
"calendar.js": "gallery.started.file.calendar-js",
"icons.svg": "gallery.started.file.sprite",
}
// release is the release build of one site's source, reduced to what
// the page needs: every file's weight, by name.
type release map[string]weight
// weighRelease builds the release from s's source and weighs every
// file in it. newSite calls it once and keeps the result on the site:
// the rail asks assetNav for the rows on every page of the tree, and a
// release build and forty gzips a page would be most of the render.
func (s *site) weighRelease() (release, error) {
dist, err := build.Dist(s.src, fiesta.Version)
if err != nil {
return nil, err
}
rel := make(release, len(dist))
for name, body := range dist {
rel[name] = weigh(body)
}
return rel, nil
}
// buildAssets reads the release build for p's page: the rows in the
// order the spec lists them (the one-file themes, fiesta.css first;
// tokens.css; each theme; shell.css; the scripts; the sprite; the
// strings, one per language), each weighed from the bytes the release
// holds. Every file the release ships is a row, or one of the files the
// page leaves out by design (LICENSE, NOTICE, SHA256SUMS, the per-icon
// SVGs the Icons page draws, the JSON the strings scripts wrap), or an
// error naming it.
func (s *site) buildAssets(p page) (assetsView, error) {
rel := s.weighed
var out assetsView
listed := map[string]bool{}
add := func(name, blurb string) error {
w, ok := rel[name]
if !ok {
return fmt.Errorf("gallery: Getting started lists %s and the release has no such file", name)
}
listed[name] = true
out.List = append(out.List, assetView{
ID: anchorID("asset", name),
Name: name,
Href: ReleaseBase + name,
Weight: s.weightIn(p.Locale, w),
Blurb: blurb,
})
return nil
}
// The one-file themes. The default theme's is fiesta.css: the one
// theme with no fiesta-.css of its own.
themes := fiesta.ThemeNames()
def := ""
for _, theme := range themes {
if _, ok := rel["fiesta-"+theme+".css"]; !ok {
if def != "" {
return assetsView{}, fmt.Errorf("gallery: both %s and %s lack a one-file theme of their own", def, theme)
}
def = theme
}
}
if def == "" {
return assetsView{}, fmt.Errorf("gallery: no theme is fiesta.css")
}
if err := add("fiesta.css", s.say(p.Locale, "gallery.started.file.bundle", "theme", def)); err != nil {
return assetsView{}, err
}
for _, theme := range themes {
if theme == def {
continue
}
if err := add("fiesta-"+theme+".css", s.say(p.Locale, "gallery.started.file.bundle", "theme", theme)); err != nil {
return assetsView{}, err
}
}
if err := add("tokens.css", s.say(p.Locale, blurbs["tokens.css"])); err != nil {
return assetsView{}, err
}
for _, theme := range themes {
if err := add(theme+".css", s.say(p.Locale, "gallery.started.file.theme", "theme", theme)); err != nil {
return assetsView{}, err
}
}
for _, name := range append([]string{"shell.css"}, scriptOrder...) {
if err := add(name, s.say(p.Locale, blurbs[name])); err != nil {
return assetsView{}, err
}
}
if err := add("icons.svg", s.say(p.Locale, blurbs["icons.svg"])); err != nil {
return assetsView{}, err
}
var langs []string
for name := range rel {
if strings.HasPrefix(name, "i18n/") && strings.HasSuffix(name, ".js") {
langs = append(langs, strings.TrimSuffix(strings.TrimPrefix(name, "i18n/"), ".js"))
}
}
sort.Strings(langs)
for _, lang := range langs {
// Each language named in itself, as the language menu names it.
if err := add("i18n/"+lang+".js", s.say(p.Locale, "gallery.started.file.strings", "language", s.say(lang, "gallery.locale.name"))); err != nil {
return assetsView{}, err
}
}
for name := range rel {
switch {
case listed[name], name == "LICENSE", name == "NOTICE", name == "SHA256SUMS",
strings.HasPrefix(name, "icons/"),
strings.HasPrefix(name, "i18n/") && strings.HasSuffix(name, ".json"):
continue
}
return assetsView{}, fmt.Errorf("gallery: the release ships %s and Getting started has no row for it", name)
}
// The total: fiesta.css and every script, each weighed on its own,
// the way a visitor downloads them.
raw, gz := 0, 0
for _, name := range append([]string{"fiesta.css"}, scriptOrder...) {
raw, gz = raw+rel[name].Raw, gz+rel[name].Gzip
}
out.Total = s.say(p.Locale, "gallery.started.total", "raw", kibUnit(p.Locale, raw), "gzip", kibUnit(p.Locale, gz))
out.Link = codeview.Highlight(``)
out.Own = codeview.Highlight(`` + "\n" + ``)
return out, nil
}
// kibUnit is kib with the unit after it, for a sentence that stands on
// its own rather than under a note saying the unit. KiB is the unit's
// own symbol, written the same in every language.
func kibUnit(locale string, n int) string { return kib(locale, n) + " KiB" }
// assetNav is one rail entry per shipped file, in the order the page
// lists them, derived from the same view the page renders from, so a
// file joins both lists on the day it ships, or neither. A release the
// view cannot be built from is reported by startedBody, which fails
// the render; the empty rail returned here is never written.
func (s *site) assetNav(p page) []navItem {
view, err := s.buildAssets(p)
if err != nil {
return nil
}
file := s.href(p.Theme, p.Locale, s.fileOf("getting-started"))
var items []navItem
for _, a := range view.List {
items = append(items, navItem{Label: a.Name, Href: file + "#" + a.ID, Code: true})
}
return items
}
// startedBody is the page a reader meets before any of the vocabulary:
// what ships, what each file is for and what it weighs, then the one
// link, the two-file form for a site serving its own copy, and how an
// upgrade reaches a page. The code blocks are translate="no": the
// address in the first is a URL the site's link gate resolves like any
// other, and neither is prose.
const startedBody = `{{define "ds-body-getting-started"}}