package gallery import ( "bytes" "embed" "fmt" "html/template" "regexp" "strings" "amadan.net/rastrillo/fiesta/example" ) // The demo application. The question a first-time reader arrives with // is "what does an app built with this look like?", and every other // page of the gallery answers a smaller one, so the Overview frames an // application before it says a word about any of them: a dashboard, a // list of requests and one request open. // // Four documents, one per view, each a real page, the way the app it // stands for would serve them: demo.html is the index (on a phone the // rail is the whole page; on a desktop the dashboard sits beside it), // and demo-dashboard.html, demo-requests.html and demo-request.html are // content pages whose back control returns to demo.html#nav-. No // script is needed to move around it, which the no-script drive holds. // // Rastrillo builds the four from its sidebar layout template; Fiesta's // shells are the layout examples' documents, so these are those // documents with the content slotted in (example.Slot) and the chrome // rewritten for the demo: the brand, the rail, the way up, the person, // the language menu. The content of each view is Rastrillo's demo in // the markup of the Fiesta example that documents each piece, in // demo/*.html. The documents go through the same framing as every // sample document (the gallery's assets, noindex, the scheme store, // frame.js first), with the layouts' own link rewrite skipped: the demo // marks every live link of its own data-gallery-nav. //go:embed demo/*.html var demoSource embed.FS // demoTemplates are the three views' content, by file name. var demoTemplates = template.Must(template.ParseFS(demoSource, "demo/*.html")) // demoViews are the demo's four documents, the index first. var demoViews = []string{"index", "dashboard", "requests", "request"} // demoFile is one view's file name under the theme and locale // directory: demo.html for the index, demo-.html for the rest. // Each is one of reservedDocuments. func demoFile(view string) string { if view == "index" { return "demo.html" } return "demo-" + view + ".html" } // demoHref is the demo application's address: its index. func (s *site) demoHref(theme, locale string) string { return s.href(theme, locale, demoFile("index")) } // demoPageHref is one content page of the demo application. func (s *site) demoPageHref(theme, locale, view string) string { return s.href(theme, locale, demoFile(view)) } // demoData is what a view's content executes against: the three pages // it links, the Overview it leaves for, and the two sentences of the // gallery's own it carries (the way back, and the request view's // closing callout). type demoData struct { Dashboard, Requests, Request string Overview, Back, Callout string } // demoBackHeld and demoCalloutHeld stand in a view's content for the // two sentences of the gallery's own it carries while the content goes // through localMarkup: they are in the page's language already, from // say, and are not sample strings to look up. Neither is a letter, so // the walker leaves them as written; demo puts the sentences in after. const ( demoBackHeld = "\ue001" demoCalloutHeld = "\ue002" ) // demoContent is one view's content executed against data, as the // layout's slot takes it. func demoContent(view string, data demoData) (string, error) { var b bytes.Buffer if err := demoTemplates.ExecuteTemplate(&b, view+".html", data); err != nil { return "", err } return strings.TrimSpace(b.String()), nil } // demoRail is the demo's rail: the two rows, the view each opens, and // its label, a sample string the rail shows in the page's language. var demoRail = []struct{ id, view, label string }{ {"nav-dashboard", "dashboard", "Dashboard"}, {"nav-requests", "requests", "Requests"}, } // demoCSS is the demo's own stylesheet: one margin, which the stat band // needs above the box under it and the layout's column does not give. const demoCSS = `` var ( // The layout's scripts the demo has no control for: no form of its // own sends (busy), and no field is enhanced (select, calendar, // datetime). demoUnused = regexp.MustCompile(`\n?`) titleTag = regexp.MustCompile(`[^<]*`) // The brand, both copies, with its words. brandLink = regexp.MustCompile(`]*>[^<]*`) // The page's title in the shell's heading, shown when the brand is // blank. shellTitle = regexp.MustCompile(`(

.*?)[^<]*(

)`) // The way up, the back strip's one link. backTag = regexp.MustCompile(`
]*>`) ) // demoPerson is the layout's person, as each of its two copies writes // it, and the demo's in its place. const ( layoutPerson = `Grace Hoppergrace@example.com` demoPerson = `Ada Lovelaceada@example.com` ) // demo builds one theme and locale's copy of the demo application: its // four documents, keyed by file name. The layout's body and each view's // content are put in the page's language before the content is slotted // in, so what demoChrome writes afterwards, the gallery's own words and // the brand, the person and the rail, is never looked up as a sample. func (s *site) demo(theme, locale string) (map[string][]byte, error) { data := demoData{ Dashboard: s.demoPageHref(theme, locale, "dashboard"), Requests: s.demoPageHref(theme, locale, "requests"), Request: s.demoPageHref(theme, locale, "request"), Overview: s.href(theme, locale, s.fileOf("overview")), Back: s.say(locale, "gallery.back"), Callout: s.say(locale, "gallery.demo.callout.body"), } out := make(map[string][]byte, len(demoViews)) for _, view := range demoViews { // The index is the shell's index document, the rest its page // document; the index shows the dashboard, which is what a // desktop sees beside the rail. layout, content := "layout-sidebar-page", view if view == "index" { layout, content = "layout-sidebar", "dashboard" } e, ok := s.m.Entry(layout) if !ok { return nil, fmt.Errorf("the manifest has no %s to build the demo application in", layout) } doc, err := example.Document(s.src, layout, e.Default) if err != nil { return nil, err } if doc, err = localBody(locale, layout, doc); err != nil { return nil, fmt.Errorf("%s: %w", layout, err) } held := data held.Back, held.Callout = demoBackHeld, demoCalloutHeld body, err := demoContent(content, held) if err != nil { return nil, err } body = localMarkup(locale, "demo", body) body = strings.NewReplacer(demoBackHeld, template.HTMLEscapeString(data.Back), demoCalloutHeld, template.HTMLEscapeString(data.Callout)).Replace(body) if doc, err = example.Slot(doc, "\n"+body+"\n"); err != nil { return nil, fmt.Errorf("%s: %w", layout, err) } if doc, err = s.demoChrome(doc, theme, locale, view); err != nil { return nil, fmt.Errorf("%s from %s: %w", demoFile(view), layout, err) } framed, err := s.framed(doc, theme, locale, e, nil) if err != nil { return nil, fmt.Errorf("%s: %w", demoFile(view), err) } out[demoFile(view)] = []byte(framed) } return out, nil } // demoChrome rewrites the layout's chrome for one view of the demo: the // title, the scripts and the demo's one rule in the head; the brand // Harbour going to the demo's index; the page's name in the shell's // heading; a rail of Dashboard and Requests with the view's marked; the // way up to the row just left (the index's own on the index); Ada // Lovelace for the layout's person; and the language menu going to this // view in each locale. Every link it writes is marked data-gallery-nav. // A piece of chrome the layout no longer carries is an error, so a // changed layout fails the render rather than shipping a demo with the // layout's own brand in it. func (s *site) demoChrome(doc, theme, locale, view string) (string, error) { app := s.say(locale, "gallery.demos.app") home := s.demoHref(theme, locale) up, current := home+"#nav-requests", "nav-requests" switch view { case "index": up, current = home, "nav-dashboard" case "dashboard": up, current = home+"#nav-dashboard", "nav-dashboard" } var nav strings.Builder nav.WriteString("") steps := []struct { what string re *regexp.Regexp with func(string) string }{ {"the title", titleTag, func(string) string { return "" + template.HTMLEscapeString(s.say(locale, "gallery.title", "page", app, "theme", theme)) + "" }}, {"the brand", brandLink, func(string) string { return `Harbour` }}, {"the shell's heading", shellTitle, func(m string) string { sub := shellTitle.FindStringSubmatch(m) return sub[1] + template.HTMLEscapeString(app) + sub[2] }}, {"the rail", shellNav, func(string) string { return nav.String() }}, {"the way up", backTag, func(m string) string { return `
` + liveLink(strings.TrimPrefix(m, `
`), up) }}, } for _, step := range steps { if !step.re.MatchString(doc) { return "", fmt.Errorf("the layout has no %s to rewrite", step.what) } doc = step.re.ReplaceAllStringFunc(doc, step.with) } if !strings.Contains(doc, layoutPerson) { return "", fmt.Errorf("the layout has no person to replace") } doc = strings.ReplaceAll(doc, layoutPerson, demoPerson) if !localeMenu.MatchString(doc) { return "", fmt.Errorf("the layout has no language menu") } var err error if doc, err = s.localeLinks(doc, locale, func(l string) string { return s.demoPageHref(theme, l, view) }); err != nil { return "", err } doc = demoUnused.ReplaceAllString(doc, "") shut := strings.LastIndex(doc, "") if shut < 0 { return "", fmt.Errorf("no for the demo's rule") } return doc[:shut] + demoCSS + "\n" + doc[shut:], nil } // demoView is the widget the Overview frames the demo application in: // the preview widget every example uses, at the page width, loading the // real index rather than a copy of it, and with no HTML tab: an // application is not a snippet to paste. func (s *site) demoView(p page) previewView { return previewView{ Group: "demo-app-0", Style: previewStyle(p.Locale, "demo"), Class: " ds-view--page", Src: s.demoHref(p.Theme, p.Locale), Title: s.say(p.Locale, "gallery.demos.app"), } }