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(`跳到正文 `)
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(``)
for _, cat := range s.cats {
open := ""
for _, c := range cat.Items {
if c.Slug == current {
open = " open"
}
}
fmt.Fprintf(&b, `%s %d `, open, e(cat.Name), len(cat.Items))
for _, c := range cat.Items {
aria := ""
if c.Slug == current {
aria = ` aria-current="page"`
}
fmt.Fprintf(&b, `%s %s `, c.Slug, aria, e(c.Name), e(c.ZhName))
}
b.WriteString(` `)
}
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, ` `, 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 `, e(slug(cat.Name)), e(cat.Name), len(cat.Items))
for _, c := range cat.Items {
fmt.Fprintf(&b, `%s %s %s `, c.Slug, c.ID, e(c.Name), e(c.ZhName))
}
b.WriteString(` `)
}
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()
}