package gallery import ( "errors" "fmt" "html/template" "strings" "amadan.net/rastrillo/fiesta/example" "amadan.net/rastrillo/fiesta/internal/codeview" ) // The preview widget: Desktop, Mobile and HTML over one frame, as // Rastrillo's gallery draws it, without its Template tab. The frame // shows the state's own document at a virtual width and a measured // height; the HTML tab shows the markup a reader copies, without the // container the manifest's wrapper puts it in, and names that // container on the line above. A grouped example (status-pill, badge, // meter) is one widget whose frame is a row document of every state, // with one code block per state. // previewView is one widget. Group names the widget's radio group, // unique on the page. Style carries the frame's virtual heights // (--ds-h and --ds-hm). Class is " ds-view--page" on the examples whose // document is a whole page and empty on the rest, with its own leading // space so the template writes class="ds-view{{.Class}}" without a // conditional; a grouped widget adds " ds-view--rows". Wrapper is the // line naming the container a wrapped sample needs; Source the markup // the HTML tab shows, formatted and highlighted. NoCopy marks a block // that offers no Copy button, as Rastrillo does for an Illustration; no // Fiesta example is one. Rows is a grouped example's code, one block // per state, and Source is empty then. type previewView struct { Group string Style template.CSS Class string Src string Wrapper template.HTML Source template.HTML Title string NoCopy bool Rows []rowCode } // rowCode is one state of a grouped example in the HTML panel. type rowCode struct { State string Source template.HTML } // HasHTML says whether the widget draws an HTML tab: it has markup to // show, as one block or as a grouped example's rows. func (v previewView) HasHTML() bool { return v.Source != "" || len(v.Rows) > 0 } // grouped names the examples shown as one widget: a row of every state // in one frame, and one code block per state. Rastrillo's three. var grouped = map[string]bool{"status-pill": true, "badge": true, "meter": true} // pageFrame reports whether an example's document is a whole page // rather than a component inside one: the layout documents, the two // shell idioms and the modal, whose backdrop is fixed to the frame's // viewport and wants a window-sized one. It picks the widget's width // class: a page lays out at a virtual 1200px, everything else at // 900px; view.css carries the argument for the two numbers. The // sign-in screens are components, as in Rastrillo: a card, not a // window. Getting this wrong costs a rendering, never a broken page. func pageFrame(e example.Entry) bool { return e.Document || strings.HasPrefix(e.Name, "shell-") || e.Name == "modal" } // previewClass is pageFrame as the template writes it. func previewClass(e example.Entry) string { if pageFrame(e) { return " ds-view--page" } return "" } // wrapperMarkup is the opening markup of the container a manifest // wrapper puts a body in, as the HTML tab names it. The seeded form's // method and action are left out: they belong to the reader's form, // and the seed's action="#" is a placeholder nobody should copy. "" is // no wrapper; anything else the manifest does not know is an error. func wrapperMarkup(w string) (string, error) { switch w { case "list": return `
`, nil case "form": return `
`, nil case "box": return `
`, nil case "stats": return `
`, nil case "": return "", nil } return "", fmt.Errorf("no such wrapper %q", w) } // unwrap is a seeded body without the outer element(s) its wrapper // names: what the HTML tab shows and Copy copies. The body has to open // with each of the wrapper's tags in turn, each carrying the wrapper's // attribute (so a bare
is not a box) and whatever else the // seed gave it, and to close them in reverse at its end; a body that // does not is an error, so a seeded document that lacks its wrapper // fails the render rather than being copied with a wrapper line over // markup that has none. Whitespace between the wrapper's tags goes // with them; the inner markup is kept as written. func unwrap(markup, wrapper string) (string, error) { open, err := wrapperMarkup(wrapper) if err != nil { return "", err } if open == "" { return markup, nil } var tags, attrs []string for _, tag := range strings.SplitAfter(open, ">") { if tag == "" { continue } fields := strings.Fields(strings.Trim(tag, "<>")) tags, attrs = append(tags, fields[0]), append(attrs, fields[1]) } rest := markup for i, tag := range tags { rest = strings.TrimLeft(rest, " \t\r\n") end := strings.IndexByte(rest, '>') if !strings.HasPrefix(rest, "<"+tag) || end < 0 { return "", fmt.Errorf("the body does not open with <%s>; its %s wrapper puts it inside %s", tag, wrapper, open) } fields := strings.Fields(rest[1:end]) if len(fields) == 0 || fields[0] != tag || !carriesAttr(fields[1:], attrs[i]) { return "", fmt.Errorf("the body opens with %s, not <%s %s>; its %s wrapper puts it inside %s", rest[:end+1], tag, attrs[i], wrapper, open) } rest = rest[end+1:] } // The outermost wrapper closes last, so its closing tag comes off // first. for i := range tags { rest = strings.TrimRight(rest, " \t\r\n") close := "" if !strings.HasSuffix(rest, close) { return "", fmt.Errorf("the body does not close with %s; its %s wrapper puts it inside %s", close, wrapper, open) } rest = rest[:len(rest)-len(close)] } // A wrapper never closed would have taken an inner element's // closing tag as its own: the inner markup has to open and close // each wrapper tag as many times. for _, tag := range tags { if strings.Count(rest, "<"+tag+">")+strings.Count(rest, "<"+tag+" ") != strings.Count(rest, "") { return "", fmt.Errorf("the body does not close its %s wrapper; it puts the markup inside %s", wrapper, open) } } return rest, nil } // carriesAttr says whether a tag's attribute fields carry name, bare or // with a value. func carriesAttr(fields []string, name string) bool { for _, f := range fields { if f == name || strings.HasPrefix(f, name+"=") { return true } } return false } // frameTitle is the frame's accessible name: the example and the // state's label, or the example's name alone where the state has no // label (the idioms' default states), rather than a name that ends in // a bare colon. func (s *site) frameTitle(locale string, e example.Entry, st example.State) string { label := s.label(locale, e, st) if label == "" { return e.Name } return s.say(locale, "gallery.frame.title", "example", e.Name, "state", label) } // wrapLine is the sentence above a wrapped sample's code, naming the // container with its opening markup in a , as Rastrillo's line // does. The sentence is the page's, escaped; the markup goes in after, // so the words around it can be any language's. func (s *site) wrapLine(locale, open string) template.HTML { sentence := template.HTMLEscapeString(s.say(locale, "gallery.wrap")) code := "" + template.HTMLEscapeString(open) + "" return template.HTML(strings.Replace(sentence, "{wrapper}", code, 1)) } // preview is one state's widget: the frame at frameURL, titled for the // example and state, at its measured heights, and the code for the // HTML tab: a component's body without its wrapper, with the line that // names the wrapper, or a layout's whole document. func (s *site) preview(p page, e example.Entry, st example.State, frameURL, code string) (previewView, error) { v := previewView{ Group: anchorID(e.Name, st.ID), Style: previewStyle(p.Locale, e.Name+"/"+st.ID), Class: previewClass(e), Src: frameURL, Title: s.frameTitle(p.Locale, e, st), } if !e.Document { open, err := wrapperMarkup(e.Wrapper) if err != nil { return previewView{}, err } if code, err = unwrap(code, e.Wrapper); err != nil { return previewView{}, err } if open != "" { v.Wrapper = s.wrapLine(p.Locale, open) } } if code != "" { v.Source = codeview.Highlight(codeview.Format(code)) } return v, nil } // rowsStyle lays a grouped document out as a row that wraps into rows // on a phone, each state under its label. It is in the document's own //