244 lines
6.1 KiB
Go
244 lines
6.1 KiB
Go
// Command site builds the static MujicaUI documentation site into site/dist.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const repoURL = "https://github.com/ZacharyZhang-NY/MujicaUI"
|
|
|
|
// component is one documented component and everything its page needs.
|
|
type component struct {
|
|
ID, Slug, Name, ZhName, Category string
|
|
Doc string // raw Markdown
|
|
Light, Dark string // absolute PNG paths, empty when missing
|
|
}
|
|
|
|
type category struct {
|
|
Name string
|
|
Items []*component
|
|
}
|
|
|
|
func main() {
|
|
root := flag.String("root", ".", "repository root")
|
|
out := flag.String("out", "site/dist", "output directory")
|
|
flag.Parse()
|
|
n, err := build(*root, *out)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("site: %d component pages written to %s\n", n, *out)
|
|
}
|
|
|
|
// build reads the repo at root and writes the site to out, returning the component page count.
|
|
func build(root, out string) (int, error) {
|
|
comps, err := loadComponents(root)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := os.RemoveAll(out); err != nil {
|
|
return 0, err
|
|
}
|
|
s := &site{root: root, out: out, comps: comps, cats: groupCategories(comps)}
|
|
if err := s.write(); err != nil {
|
|
return 0, err
|
|
}
|
|
return len(comps), nil
|
|
}
|
|
|
|
var (
|
|
docNameRe = regexp.MustCompile(`^(\d{3})-([a-z0-9-]+)\.md$`)
|
|
entryIDRe = regexp.MustCompile(`\bID:\s*"([^"]+)"`)
|
|
fieldRe = regexp.MustCompile(`\b(Name|ZhName|Category):\s*("(?:[^"\\]|\\.)*"|[A-Za-z_][A-Za-z0-9_]*)`)
|
|
constRe = regexp.MustCompile(`(?m)^\s*(?:const\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([^"]*)"`)
|
|
titleSplitR = regexp.MustCompile(`^#\s+(?:\d{3}\s+)?(\S+)\s*(.*)$`)
|
|
)
|
|
|
|
type entry struct{ Name, ZhName, Category string }
|
|
|
|
func loadComponents(root string) ([]*component, error) {
|
|
entries, err := loadGallery(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snaps, err := findSnapshots(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dir := filepath.Join(root, "docs", "components")
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var comps []*component
|
|
for _, f := range files {
|
|
g := docNameRe.FindStringSubmatch(f.Name())
|
|
if g == nil {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, f.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &component{ID: g[1], Slug: strings.TrimSuffix(f.Name(), ".md"), Doc: string(raw), Category: "Other"}
|
|
if t := titleSplitR.FindStringSubmatch(firstHeading(c.Doc)); t != nil {
|
|
c.Name, c.ZhName = t[1], t[2]
|
|
}
|
|
if e, ok := entries[c.ID]; ok {
|
|
c.Name, c.ZhName, c.Category = e.Name, e.ZhName, e.Category
|
|
}
|
|
if c.Name == "" {
|
|
return nil, fmt.Errorf("site: %s has no heading and no gallery entry", f.Name())
|
|
}
|
|
key := normKey(c.Name)
|
|
c.Light, c.Dark = snaps[key+"-light"], snaps[key+"-dark"]
|
|
comps = append(comps, c)
|
|
}
|
|
if len(comps) == 0 {
|
|
return nil, fmt.Errorf("site: no component docs in %s", dir)
|
|
}
|
|
return comps, nil
|
|
}
|
|
|
|
func firstHeading(doc string) string {
|
|
for _, l := range strings.Split(doc, "\n") {
|
|
if strings.HasPrefix(l, "# ") {
|
|
return strings.TrimSpace(l)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// loadGallery scans examples/gallery/pages_*.go for page entries without importing the package.
|
|
func loadGallery(root string) (map[string]entry, error) {
|
|
files, err := filepath.Glob(filepath.Join(root, "examples", "gallery", "pages_*.go"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
consts := map[string]string{}
|
|
var srcs []string
|
|
for _, f := range files {
|
|
b, err := os.ReadFile(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
src := string(b)
|
|
srcs = append(srcs, src)
|
|
for _, m := range constRe.FindAllStringSubmatch(src, -1) {
|
|
consts[m[1]] = m[2]
|
|
}
|
|
}
|
|
out := map[string]entry{}
|
|
for _, src := range srcs {
|
|
locs := entryIDRe.FindAllStringSubmatchIndex(src, -1)
|
|
for i, loc := range locs {
|
|
end := len(src)
|
|
if i+1 < len(locs) {
|
|
end = locs[i+1][0]
|
|
}
|
|
id := src[loc[2]:loc[3]]
|
|
var e entry
|
|
for _, f := range fieldRe.FindAllStringSubmatch(src[loc[1]:end], -1) {
|
|
v := f[2]
|
|
if strings.HasPrefix(v, `"`) {
|
|
if u, err := strconv.Unquote(v); err == nil {
|
|
v = u
|
|
}
|
|
} else if cv, ok := consts[v]; ok {
|
|
v = cv
|
|
} else {
|
|
continue // a variable such as Code: fooCode is not a field we want
|
|
}
|
|
switch f[1] {
|
|
case "Name":
|
|
if e.Name == "" {
|
|
e.Name = v
|
|
}
|
|
case "ZhName":
|
|
if e.ZhName == "" {
|
|
e.ZhName = v
|
|
}
|
|
case "Category":
|
|
if e.Category == "" {
|
|
e.Category = v
|
|
}
|
|
}
|
|
}
|
|
if e.Name != "" && e.Category != "" {
|
|
out[id] = e
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// findSnapshots indexes every PNG under any testdata/snapshots/darwin directory by normalized base name.
|
|
func findSnapshots(root string) (map[string]string, error) {
|
|
out := map[string]string{}
|
|
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !d.IsDir() {
|
|
return nil
|
|
}
|
|
name := d.Name()
|
|
if p != root && (strings.HasPrefix(name, ".") || name == "node_modules" || name == "dist") {
|
|
return filepath.SkipDir
|
|
}
|
|
if filepath.ToSlash(p) != "testdata/snapshots/darwin" && !strings.HasSuffix(filepath.ToSlash(p), "/testdata/snapshots/darwin") {
|
|
return nil
|
|
}
|
|
pngs, err := filepath.Glob(filepath.Join(p, "*.png"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Strings(pngs)
|
|
for _, f := range pngs {
|
|
base := strings.TrimSuffix(filepath.Base(f), ".png")
|
|
if strings.HasSuffix(base, ".got") {
|
|
continue
|
|
}
|
|
i := strings.LastIndex(base, "-")
|
|
if i < 0 {
|
|
continue
|
|
}
|
|
key := normKey(base[:i]) + base[i:]
|
|
if _, ok := out[key]; !ok {
|
|
out[key] = f
|
|
}
|
|
}
|
|
return filepath.SkipDir
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
// normKey folds snake_case and CamelCase names to one key, so OAuthButtons meets oauth_buttons.
|
|
func normKey(s string) string {
|
|
return strings.ToLower(strings.NewReplacer("_", "", "-", "").Replace(s))
|
|
}
|
|
|
|
func groupCategories(comps []*component) []*category {
|
|
idx := map[string]*category{}
|
|
var cats []*category
|
|
for _, c := range comps {
|
|
cat, ok := idx[c.Category]
|
|
if !ok {
|
|
cat = &category{Name: c.Category}
|
|
idx[c.Category] = cat
|
|
cats = append(cats, cat)
|
|
}
|
|
cat.Items = append(cat.Items, c)
|
|
}
|
|
return cats
|
|
}
|