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 "bytes"
5 "embed"
6 "fmt"
7 "html/template"
8 "regexp"
9 "strings"
10
11 "amadan.net/rastrillo/fiesta/example"
12)
13
14// The demo application. The question a first-time reader arrives with
15// is "what does an app built with this look like?", and every other
16// page of the gallery answers a smaller one, so the Overview frames an
17// application before it says a word about any of them: a dashboard, a
18// list of requests and one request open.
19//
20// Four documents, one per view, each a real page, the way the app it
21// stands for would serve them: demo.html is the index (on a phone the
22// rail is the whole page; on a desktop the dashboard sits beside it),
23// and demo-dashboard.html, demo-requests.html and demo-request.html are
24// content pages whose back control returns to demo.html#nav-<row>. No
25// script is needed to move around it, which the no-script drive holds.
26//
27// Rastrillo builds the four from its sidebar layout template; Fiesta's
28// shells are the layout examples' documents, so these are those
29// documents with the content slotted in (example.Slot) and the chrome
30// rewritten for the demo: the brand, the rail, the way up, the person,
31// the language menu. The content of each view is Rastrillo's demo in
32// the markup of the Fiesta example that documents each piece, in
33// demo/*.html. The documents go through the same framing as every
34// sample document (the gallery's assets, noindex, the scheme store,
35// frame.js first), with the layouts' own link rewrite skipped: the demo
36// marks every live link of its own data-gallery-nav.
37
38//go:embed demo/*.html
39var demoSource embed.FS
40
41// demoTemplates are the three views' content, by file name.
42var demoTemplates = template.Must(template.ParseFS(demoSource, "demo/*.html"))
43
44// demoViews are the demo's four documents, the index first.
45var demoViews = []string{"index", "dashboard", "requests", "request"}
46
47// demoFile is one view's file name under the theme and locale
48// directory: demo.html for the index, demo-<view>.html for the rest.
49// Each is one of reservedDocuments.
50func demoFile(view string) string {
51 if view == "index" {
52 return "demo.html"
53 }
54 return "demo-" + view + ".html"
55}
56
57// demoHref is the demo application's address: its index.
58func (s *site) demoHref(theme, locale string) string {
59 return s.href(theme, locale, demoFile("index"))
60}
61
62// demoPageHref is one content page of the demo application.
63func (s *site) demoPageHref(theme, locale, view string) string {
64 return s.href(theme, locale, demoFile(view))
65}
66
67// demoData is what a view's content executes against: the three pages
68// it links, the Overview it leaves for, and the two sentences of the
69// gallery's own it carries (the way back, and the request view's
70// closing callout).
71type demoData struct {
72 Dashboard, Requests, Request string
73 Overview, Back, Callout string
74}
75
76// demoBackHeld and demoCalloutHeld stand in a view's content for the
77// two sentences of the gallery's own it carries while the content goes
78// through localMarkup: they are in the page's language already, from
79// say, and are not sample strings to look up. Neither is a letter, so
80// the walker leaves them as written; demo puts the sentences in after.
81const (
82 demoBackHeld = "\ue001"
83 demoCalloutHeld = "\ue002"
84)
85
86// demoContent is one view's content executed against data, as the
87// layout's slot takes it.
88func demoContent(view string, data demoData) (string, error) {
89 var b bytes.Buffer
90 if err := demoTemplates.ExecuteTemplate(&b, view+".html", data); err != nil {
91 return "", err
92 }
93 return strings.TrimSpace(b.String()), nil
94}
95
96// demoRail is the demo's rail: the two rows, the view each opens, and
97// its label, a sample string the rail shows in the page's language.
98var demoRail = []struct{ id, view, label string }{
99 {"nav-dashboard", "dashboard", "Dashboard"},
100 {"nav-requests", "requests", "Requests"},
101}
102
103// demoCSS is the demo's own stylesheet: one margin, which the stat band
104// needs above the box under it and the layout's column does not give.
105const demoCSS = `<style>[rst-stats] { margin-block-end: var(--rst-sp-5); }</style>`
106
107var (
108 // The layout's scripts the demo has no control for: no form of its
109 // own sends (busy), and no field is enhanced (select, calendar,
110 // datetime).
111 demoUnused = regexp.MustCompile(`<script defer src="\.\./\.\./js/(busy|select|calendar|datetime)\.js"></script>\n?`)
112 titleTag = regexp.MustCompile(`<title>[^<]*</title>`)
113 // The brand, both copies, with its words.
114 brandLink = regexp.MustCompile(`<a rst-shell-brand\b[^>]*>[^<]*</a>`)
115 // The page's title in the shell's heading, shown when the brand is
116 // blank.
117 shellTitle = regexp.MustCompile(`(<h1 rst-shell-title><span>.*?</span><span>)[^<]*(</span></h1>)`)
118 // The way up, the back strip's one link.
119 backTag = regexp.MustCompile(`<div rst-shell-back><a\b[^>]*>`)
120)
121
122// demoPerson is the layout's person, as each of its two copies writes
123// it, and the demo's in its place.
124const (
125 layoutPerson = `<span rst-person-av aria-hidden="true">G</span><span rst-person-name>Grace Hopper</span><span rst-person-email>grace@example.com</span>`
126 demoPerson = `<span rst-person-av aria-hidden="true">A</span><span rst-person-name>Ada Lovelace</span><span rst-person-email>ada@example.com</span>`
127)
128
129// demo builds one theme and locale's copy of the demo application: its
130// four documents, keyed by file name. The layout's body and each view's
131// content are put in the page's language before the content is slotted
132// in, so what demoChrome writes afterwards, the gallery's own words and
133// the brand, the person and the rail, is never looked up as a sample.
134func (s *site) demo(theme, locale string) (map[string][]byte, error) {
135 data := demoData{
136 Dashboard: s.demoPageHref(theme, locale, "dashboard"),
137 Requests: s.demoPageHref(theme, locale, "requests"),
138 Request: s.demoPageHref(theme, locale, "request"),
139 Overview: s.href(theme, locale, s.fileOf("overview")),
140 Back: s.say(locale, "gallery.back"),
141 Callout: s.say(locale, "gallery.demo.callout.body"),
142 }
143 out := make(map[string][]byte, len(demoViews))
144 for _, view := range demoViews {
145 // The index is the shell's index document, the rest its page
146 // document; the index shows the dashboard, which is what a
147 // desktop sees beside the rail.
148 layout, content := "layout-sidebar-page", view
149 if view == "index" {
150 layout, content = "layout-sidebar", "dashboard"
151 }
152 e, ok := s.m.Entry(layout)
153 if !ok {
154 return nil, fmt.Errorf("the manifest has no %s to build the demo application in", layout)
155 }
156 doc, err := example.Document(s.src, layout, e.Default)
157 if err != nil {
158 return nil, err
159 }
160 if doc, err = localBody(locale, layout, doc); err != nil {
161 return nil, fmt.Errorf("%s: %w", layout, err)
162 }
163 held := data
164 held.Back, held.Callout = demoBackHeld, demoCalloutHeld
165 body, err := demoContent(content, held)
166 if err != nil {
167 return nil, err
168 }
169 body = localMarkup(locale, "demo", body)
170 body = strings.NewReplacer(demoBackHeld, template.HTMLEscapeString(data.Back), demoCalloutHeld, template.HTMLEscapeString(data.Callout)).Replace(body)
171 if doc, err = example.Slot(doc, "\n"+body+"\n"); err != nil {
172 return nil, fmt.Errorf("%s: %w", layout, err)
173 }
174 if doc, err = s.demoChrome(doc, theme, locale, view); err != nil {
175 return nil, fmt.Errorf("%s from %s: %w", demoFile(view), layout, err)
176 }
177 framed, err := s.framed(doc, theme, locale, e, nil)
178 if err != nil {
179 return nil, fmt.Errorf("%s: %w", demoFile(view), err)
180 }
181 out[demoFile(view)] = []byte(framed)
182 }
183 return out, nil
184}
185
186// demoChrome rewrites the layout's chrome for one view of the demo: the
187// title, the scripts and the demo's one rule in the head; the brand
188// Harbour going to the demo's index; the page's name in the shell's
189// heading; a rail of Dashboard and Requests with the view's marked; the
190// way up to the row just left (the index's own on the index); Ada
191// Lovelace for the layout's person; and the language menu going to this
192// view in each locale. Every link it writes is marked data-gallery-nav.
193// A piece of chrome the layout no longer carries is an error, so a
194// changed layout fails the render rather than shipping a demo with the
195// layout's own brand in it.
196func (s *site) demoChrome(doc, theme, locale, view string) (string, error) {
197 app := s.say(locale, "gallery.demos.app")
198 home := s.demoHref(theme, locale)
199 up, current := home+"#nav-requests", "nav-requests"
200 switch view {
201 case "index":
202 up, current = home, "nav-dashboard"
203 case "dashboard":
204 up, current = home+"#nav-dashboard", "nav-dashboard"
205 }
206 var nav strings.Builder
207 nav.WriteString("<nav rst-shell-nav>")
208 for _, item := range demoRail {
209 nav.WriteString(`<a id="` + item.id + `" data-gallery-nav href="` + s.demoPageHref(theme, locale, item.view) + `"`)
210 if item.id == current {
211 nav.WriteString(` aria-current="page"`)
212 }
213 nav.WriteString(">" + localMarkup(locale, "demo", item.label) + "</a>")
214 }
215 nav.WriteString("</nav>")
216
217 steps := []struct {
218 what string
219 re *regexp.Regexp
220 with func(string) string
221 }{
222 {"the title", titleTag, func(string) string {
223 return "<title>" + template.HTMLEscapeString(s.say(locale, "gallery.title", "page", app, "theme", theme)) + "</title>"
224 }},
225 {"the brand", brandLink, func(string) string {
226 return `<a rst-shell-brand data-gallery-nav href="` + home + `">Harbour</a>`
227 }},
228 {"the shell's heading", shellTitle, func(m string) string {
229 sub := shellTitle.FindStringSubmatch(m)
230 return sub[1] + template.HTMLEscapeString(app) + sub[2]
231 }},
232 {"the rail", shellNav, func(string) string { return nav.String() }},
233 {"the way up", backTag, func(m string) string {
234 return `<div rst-shell-back>` + liveLink(strings.TrimPrefix(m, `<div rst-shell-back>`), up)
235 }},
236 }
237 for _, step := range steps {
238 if !step.re.MatchString(doc) {
239 return "", fmt.Errorf("the layout has no %s to rewrite", step.what)
240 }
241 doc = step.re.ReplaceAllStringFunc(doc, step.with)
242 }
243 if !strings.Contains(doc, layoutPerson) {
244 return "", fmt.Errorf("the layout has no person to replace")
245 }
246 doc = strings.ReplaceAll(doc, layoutPerson, demoPerson)
247 if !localeMenu.MatchString(doc) {
248 return "", fmt.Errorf("the layout has no language menu")
249 }
250 var err error
251 if doc, err = s.localeLinks(doc, locale, func(l string) string { return s.demoPageHref(theme, l, view) }); err != nil {
252 return "", err
253 }
254 doc = demoUnused.ReplaceAllString(doc, "")
255 shut := strings.LastIndex(doc, "</head>")
256 if shut < 0 {
257 return "", fmt.Errorf("no </head> for the demo's rule")
258 }
259 return doc[:shut] + demoCSS + "\n" + doc[shut:], nil
260}
261
262// demoView is the widget the Overview frames the demo application in:
263// the preview widget every example uses, at the page width, loading the
264// real index rather than a copy of it, and with no HTML tab: an
265// application is not a snippet to paste.
266func (s *site) demoView(p page) previewView {
267 return previewView{
268 Group: "demo-app-0",
269 Style: previewStyle(p.Locale, "demo"),
270 Class: " ds-view--page",
271 Src: s.demoHref(p.Theme, p.Locale),
272 Title: s.say(p.Locale, "gallery.demos.app"),
273 }
274}
275