// 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