// Package example reads the design system's examples: one document // per component state under examples/, listed by examples/examples.json. // It is public so that an application which embeds Fiesta can read the // same examples its gallery and its tests do, and it links no browser. // // Every state is its own document, so its ids, labels and fixed // overlays never meet another state's. Markup returns the body of one, // exactly as the file holds it; Document returns the whole file, for // the layouts, whose and
are part of what they show; // Slot fills a layout's content region. package example import ( "encoding/json" "errors" "fmt" "io/fs" "strings" "golang.org/x/net/html" ) // Manifest is examples/examples.json: every example in gallery order, // and the idiom table that says which example and state each of the // design system's markup idioms became. type Manifest struct { Examples []Entry `json:"examples"` Idioms map[string]Idiom `json:"idioms"` } // Entry is one example: a directory under examples/ holding one // document per state. // // Kind is one of component, idiom, layout, format and screen. Document // marks a layout, whose states are whole pages with a slot rather than // markup to drop into one. Wrapper names the container a component's // markup keeps as its outermost element: "", list, form, box or stats. // Scripts lists the optional scripts the markup needs, by file name. // Default is the state later readers use. type Entry struct { Name string `json:"name"` Title string `json:"title"` Family string `json:"family"` Kind string `json:"kind"` Document bool `json:"document"` Wrapper string `json:"wrapper"` Scripts []string `json:"scripts"` Default string `json:"default"` States []State `json:"states"` } // State is one state of an example: its id (the file's stem), its // English label, and the application routes its markup links to, // sorted. type State struct { ID string `json:"id"` Label string `json:"label"` SampleURLs []string `json:"sampleURLs"` } // Idiom names the example and state an idiom key maps to. type Idiom struct { Example string `json:"example"` State string `json:"state"` } // Piece is one state's markup with its address. type Piece struct { Name, State, Markup string } // Load reads examples/examples.json from fsys, which is fiesta.FS() or // any tree laid out the same way. func Load(fsys fs.FS) (*Manifest, error) { b, err := fs.ReadFile(fsys, "examples/examples.json") if err != nil { return nil, fmt.Errorf("example: %w", err) } var m Manifest if err := json.Unmarshal(b, &m); err != nil { return nil, fmt.Errorf("example: examples/examples.json: %w", err) } return &m, nil } // Entry returns the example called name. func (m *Manifest) Entry(name string) (Entry, bool) { for _, e := range m.Examples { if e.Name == name { return e, true } } return Entry{}, false } // Default returns the id of the state later readers use for name: the // one the manifest names as its default. func (m *Manifest) Default(name string) (string, bool) { e, ok := m.Entry(name) if !ok { return "", false } return e.Default, true } // Idiom returns the example and state the idiom key maps to, so // Idiom("dropdown") gives ("dropdown", "menu-idiom", true). func (m *Manifest) Idiom(key string) (name, state string, ok bool) { ref, ok := m.Idioms[key] if !ok { return "", "", false } return ref.Example, ref.State, true } // file is the path of one state's document inside the tree. A name or // state holding a path separator is refused rather than resolved: the // two are identifiers from the manifest, never paths. func file(name, state string) (string, error) { if name == "" || state == "" || strings.ContainsAny(name, "/\\") || strings.ContainsAny(state, "/\\") { return "", fmt.Errorf("example: %q/%q is not an example state", name, state) } return "examples/" + name + "/" + state + ".html", nil } // Document returns one state's whole document. func Document(fsys fs.FS, name, state string) (string, error) { p, err := file(name, state) if err != nil { return "", err } b, err := fs.ReadFile(fsys, p) if err != nil { return "", fmt.Errorf("example: %w", err) } return string(b), nil } // Markup returns the bytes between and of one state's // document, exactly as written, trimmed of one leading and one // trailing newline. The tokenizer is used only to find the two tags, // so a nested container or a inside the body is bytes like // any other, and nothing is re-serialised. func Markup(fsys fs.FS, name, state string) (string, error) { doc, err := Document(fsys, name, state) if err != nil { return "", err } start, end, err := body(doc) if err != nil { return "", fmt.Errorf("example: %s/%s: %w", name, state, err) } return strings.TrimSuffix(strings.TrimPrefix(doc[start:end], "\n"), "\n"), nil } // body returns the offsets of the first byte after the start // tag and of the end tag. func body(doc string) (start, end int, err error) { z := html.NewTokenizer(strings.NewReader(doc)) offset := 0 start, end = -1, -1 for { tt := z.Next() raw := len(z.Raw()) switch tt { case html.ErrorToken: if start < 0 { return 0, 0, errors.New("no tag") } return 0, 0, errors.New("no tag") case html.StartTagToken: if n, _ := z.TagName(); start < 0 && string(n) == "body" { start = offset + raw } case html.EndTagToken: if n, _ := z.TagName(); start >= 0 && string(n) == "body" { return start, offset, nil } } offset += raw } } // Element returns the one element in markup that carries attribute, // from its start tag to its matching end tag, exactly as written. // // The examples show a component inside the row it sits in, and a // reader that wants the bare component (the gallery's header, a drive // that boxes a dropdown on its own, a golden test against a partial) // would otherwise take the row with it. It is an error when no element // carries attribute, when two do at the outermost level (the second // would go unseen), and when the element is never closed (markup cut // short would otherwise read as a smaller component). A carrier inside // another counts once, as the outer one. func Element(markup, attribute string) (string, error) { c, err := carrier(markup, attribute) if err != nil { return "", err } return markup[c.start:c.end], nil } // Contents returns what lies between that element's start and end tags, // exactly as written. A void element has none, and is an error, since // the caller asked for a container. func Contents(markup, attribute string) (string, error) { c, err := carrier(markup, attribute) if err != nil { return "", err } if c.void { return "", fmt.Errorf("example: the element carrying %s is a void element and holds nothing", attribute) } return markup[c.open:c.close], nil } // span is where one element lies in its markup: start to end is the // whole element, open to close what its tags enclose. type span struct { start, open, close, end int void bool } // voidElements have no end tag, so the element is its start tag alone; // counting depth for them would wait for a that never comes. var voidElements = map[string]bool{ "area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true, "input": true, "link": true, "meta": true, "source": true, "track": true, "wbr": true, } // carrier finds the one outermost element carrying attribute. Only // elements of the carrier's own tag name are counted on the way to its // end tag: a