package main import ( "encoding/json" "fmt" "html" "io" "os" "path" "path/filepath" "regexp" "strings" ) type site struct { root, out string comps []*component cats []*category } // extraPages are repo documents published as their own pages. var extraPages = []struct{ Slug, Src, Title string }{ {"readme", "README.md", "README"}, {"compat", "docs/compat.md", "兼容与平台依赖"}, {"upstream", "docs/upstream.md", "上游归并"}, } var compLinkRe = regexp.MustCompile(`^(?:\.\./|docs/components/|\./)?(\d{3}-[a-z0-9-]+)\.md(#.*)?$`) // link rewrites repository-relative hrefs to site routes or GitHub URLs. func (s *site) link(from string) func(string) string { return func(href string) string { if strings.Contains(href, "://") || strings.HasPrefix(href, "#") || strings.HasPrefix(href, "mailto:") { return href } if g := compLinkRe.FindStringSubmatch(href); g != nil { return "/c/" + g[1] + "/" + g[2] } resolved := path.Clean(path.Join(path.Dir(from), href)) for _, p := range extraPages { if resolved == p.Src { return "/" + p.Slug + "/" } } if resolved == "docs/components" { return "/#components" } return repoURL + "/blob/main/" + resolved } } func (s *site) write() error { if err := copyDir(filepath.Join(s.root, "site"), s.out, func(name string) bool { return strings.HasSuffix(name, ".css") || strings.HasSuffix(name, ".js") }); err != nil { return err } if err := os.MkdirAll(filepath.Join(s.out, "img"), 0o755); err != nil { return err } if err := s.writeFile("index.html", s.index()); err != nil { return err } if err := s.writeFile("404.html", s.page("页面不存在", "", `

页面不存在

找不到这个地址。返回组件目录。

`)); err != nil { return err } for _, p := range extraPages { raw, err := os.ReadFile(filepath.Join(s.root, p.Src)) if err != nil { return err } body := `
` + markdown{link: s.link(p.Src)}.render(string(raw)) + `
` if err := s.writeFile(p.Slug+"/index.html", s.page(p.Title, "", body)); err != nil { return err } } for _, c := range s.comps { for _, src := range []string{c.Light, c.Dark} { if src != "" { if err := copyFile(src, filepath.Join(s.out, "img", filepath.Base(src))); err != nil { return err } } } if err := s.writeFile("c/"+c.Slug+"/index.html", s.componentPage(c)); err != nil { return err } } return nil } func (s *site) writeFile(rel, content string) error { p := filepath.Join(s.out, filepath.FromSlash(rel)) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return err } return os.WriteFile(p, []byte(content), 0o644) } const themeBoot = `` func (s *site) page(title, current, main string) string { e := html.EscapeString var b strings.Builder b.WriteString(``) b.WriteString(`` + e(title) + ` · MujicaUI`) b.WriteString(`` + themeBoot + ``) b.WriteString(`
MujicaUI`) b.WriteString(`
`) if current != "" { b.WriteString(`
`) b.WriteString(s.sidebar(current)) b.WriteString(`
` + main + `
`) } else { b.WriteString(`
` + main + `
`) } b.WriteString(``) b.WriteString(``) return b.String() } func (s *site) sidebar(current string) string { e := html.EscapeString var b strings.Builder b.WriteString(``) return b.String() } func (s *site) componentPage(c *component) string { e := html.EscapeString var b strings.Builder fmt.Fprintf(&b, `

%s · #%s

`, e(slug(c.Category)), e(c.Category), c.ID) b.WriteString(`
`) for _, v := range []struct{ src, mode, label string }{{c.Light, "light", "浅色"}, {c.Dark, "dark", "深色"}} { fmt.Fprintf(&b, `
`, v.mode) if v.src != "" { fmt.Fprintf(&b, `%s %s主题渲染快照`, e(filepath.Base(v.src)), e(c.Name), v.label) } else { b.WriteString(`
暂无快照
`) } fmt.Fprintf(&b, `
%s · 渲染快照
`, v.label) } b.WriteString(`
`) b.WriteString(`
` + markdown{link: s.link("docs/components/" + c.Slug + ".md")}.render(c.Doc) + `
`) b.WriteString(s.pager(c)) return s.page(c.Name+" "+c.ZhName, c.Slug, b.String()) } func (s *site) pager(c *component) string { var prev, next *component for i, x := range s.comps { if x == c { if i > 0 { prev = s.comps[i-1] } if i+1 < len(s.comps) { next = s.comps[i+1] } } } var b strings.Builder b.WriteString(``) return b.String() } type searchItem struct { ID string `json:"id"` Slug string `json:"s"` Name string `json:"n"` Zh string `json:"z"` Cat string `json:"c"` } func (s *site) index() string { e := html.EscapeString var b strings.Builder shots := 0 for _, c := range s.comps { if c.Light != "" || c.Dark != "" { shots++ } } b.WriteString(`

Court-gothic components for MyGo

MujicaUI

`) b.WriteString(``) b.WriteString(`

说明:这些组件是原生 Go(MyGo)控件,在桌面窗口里由 GPU 自绘。本站展示的是渲染快照图片,浏览器不运行任何组件代码。
`) b.WriteString(`Note: the components are native Go (MyGo) widgets. The images are rendered snapshots; your browser runs no component code.

`) b.WriteString(`

开始使用GitHub 源码

`) b.WriteString(``) b.WriteString(`

分类

`) for _, cat := range s.cats { fmt.Fprintf(&b, `%s%d`, e(slug(cat.Name)), e(cat.Name), len(cat.Items)) } b.WriteString(`
`) for _, cat := range s.cats { fmt.Fprintf(&b, `

%s %d

`) } items := make([]searchItem, 0, len(s.comps)) for _, c := range s.comps { items = append(items, searchItem{c.ID, c.Slug, c.Name, c.ZhName, c.Category}) } data, err := json.Marshal(items) if err != nil { panic(err) // plain strings always marshal } b.WriteString(``) return s.page("组件", "", b.String()) } func copyDir(src, dst string, keep func(string) bool) error { files, err := os.ReadDir(src) if err != nil { return err } for _, f := range files { if f.IsDir() || !keep(f.Name()) { continue } if err := copyFile(filepath.Join(src, f.Name()), filepath.Join(dst, f.Name())); err != nil { return err } } return nil } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } out, err := os.Create(dst) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() return err } return out.Close() }