Mercurial Hosting > qwb
view qwb.go @ 0:ac64aa92dea1
initial
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Fri, 13 Mar 2026 13:13:07 +0500 |
| parents | |
| children | 3222f88c0afe |
line wrap: on
line source
package main import ( "bufio" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) const siteTitle = "My Site" const tmpl = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{TITLE}}</title> <link rel="stylesheet" href="/x.css"> </head> <body> <header><h1>` + siteTitle + `</h1></header> <nav>{{NAV}}</nav> <main>{{CONTENT}}</main> <footer> <p>` + siteTitle + `</p> <p>© Built with <a href="https://hg.reactionary.software/qwb">qwb</a></p> </footer> </body> </html>` // section — top-level directory, each with its own index and pages type section struct { title string index string // /section/index.html, or "" if none pages []page // non-index .md files within this section } type page struct { title string path string } // collectSections scans one level deep: root files go into a synthetic root // section, each subdirectory becomes its own section. func collectSections(root string) (rootSection section, subs []section) { rootSection.title = siteTitle entries, _ := os.ReadDir(root) for _, e := range entries { full := filepath.Join(root, e.Name()) if e.IsDir() { s := scanSection(full, root) if s.index != "" || len(s.pages) > 0 { subs = append(subs, s) } continue } if strings.HasSuffix(e.Name(), ".md") { if e.Name() == "index.md" { rootSection.index = "/index.html" } else { rel, _ := filepath.Rel(root, full) rootSection.pages = append(rootSection.pages, page{ title: titleFromName(e.Name()), path: "/" + strings.TrimSuffix(rel, ".md") + ".html", }) } } } return } func scanSection(dir, root string) section { s := section{title: titleFromName(filepath.Base(dir))} entries, _ := os.ReadDir(dir) for _, e := range entries { if e.IsDir() { continue // only one level deep } if !strings.HasSuffix(e.Name(), ".md") { continue } rel, _ := filepath.Rel(root, filepath.Join(dir, e.Name())) htmlPath := "/" + strings.TrimSuffix(rel, ".md") + ".html" if e.Name() == "index.md" { s.index = htmlPath } else { s.pages = append(s.pages, page{titleFromName(e.Name()), htmlPath}) } } return s } func navLink(b *strings.Builder, p page, cur string) { if p.path == cur { fmt.Fprintf(b, "<li><b><a href=\"%s\">%s</a></b></li>\n", p.path, p.title) } else { fmt.Fprintf(b, "<li><a href=\"%s\">%s</a></li>\n", p.path, p.title) } } // buildNav produces one or two <ul> blocks. // Top <ul>: root index + root loose pages + one entry per sub-section (via its index). // Second <ul>: pages of the active sub-section, only when section has 2+ pages. func buildNav(rootSec section, subs []section, cur string) string { var b strings.Builder b.WriteString("<ul>\n") if rootSec.index != "" { navLink(&b, page{"Home", rootSec.index}, cur) } for _, p := range rootSec.pages { navLink(&b, p, cur) } for _, s := range subs { link := s.index if link == "" && len(s.pages) > 0 { link = s.pages[0].path } if link == "" { continue } navLink(&b, page{s.title, link}, cur) } b.WriteString("</ul>\n") // Find which sub-section cur belongs to for _, s := range subs { if !sectionContains(s, cur) { continue } // Only render sub-nav if there are 2+ addressable pages in the section total := len(s.pages) if s.index != "" { total++ } if total < 2 { break } b.WriteString("<ul>\n") if s.index != "" { navLink(&b, page{"Index", s.index}, cur) } for _, p := range s.pages { navLink(&b, p, cur) } b.WriteString("</ul>\n") break } return b.String() } func sectionContains(s section, cur string) bool { if s.index == cur { return true } for _, p := range s.pages { if p.path == cur { return true } } return false } func mdToHTML(path string) (string, error) { cmd := exec.Command("lowdown", "-T", "html", "--html-no-skiphtml", "--html-no-escapehtml") f, err := os.Open(path) if err != nil { return "", err } defer f.Close() var buf strings.Builder cmd.Stdin = f cmd.Stdout = &buf cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", err } return buf.String(), nil } func titleFromName(name string) string { name = strings.TrimSuffix(name, ".md") name = strings.ReplaceAll(name, "-", " ") if len(name) > 0 { name = strings.ToUpper(name[:1]) + name[1:] } return name } func fixLinks(s string) string { return strings.NewReplacer( ".md)", ".html)", ".md\"", ".html\"", ".md'", ".html'", ".md#", ".html#", ".md>", ".html>", ".md ", ".html ", ".md,", ".html,", ).Replace(s) } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err } func main() { if len(os.Args) != 3 { fmt.Fprintln(os.Stderr, "usage: qwb <in> <out>") os.Exit(1) } src, out := os.Args[1], os.Args[2] if entries, err := os.ReadDir(out); err == nil && len(entries) > 0 { fmt.Fprintf(os.Stderr, "qwb: %s is not empty, overwrite? [y/N] ", out) s, _ := bufio.NewReader(os.Stdin).ReadString('\n') if strings.TrimSpace(strings.ToLower(s)) != "y" { fmt.Fprintln(os.Stderr, "aborted") os.Exit(1) } if err := os.RemoveAll(out); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } rootSec, subs := collectSections(src) err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { if err != nil { return err } rel, _ := filepath.Rel(src, path) outpath := filepath.Join(out, rel) if d.IsDir() { return os.MkdirAll(outpath, 0755) } if !strings.HasSuffix(path, ".md") { return copyFile(path, outpath) } body, err := mdToHTML(path) if err != nil { return err } body = fixLinks(body) cur := "/" + strings.TrimSuffix(rel, ".md") + ".html" title := siteTitle if filepath.Base(path) != "index.md" { title = siteTitle + " | " + titleFromName(filepath.Base(path)) } pg := strings.ReplaceAll(tmpl, "{{TITLE}}", title) pg = strings.ReplaceAll(pg, "{{NAV}}", buildNav(rootSec, subs, cur)) pg = strings.ReplaceAll(pg, "{{CONTENT}}", body) outpath = strings.TrimSuffix(outpath, ".md") + ".html" return os.WriteFile(outpath, []byte(pg), 0644) }) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
