Mercurial Hosting > qwb
comparison qwb.go @ 5:125e599b1217
agbubu
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Tue, 17 Mar 2026 22:18:02 +0500 |
| parents | ce2b6dde4c10 |
| children | bd0d3a189f5b |
comparison
equal
deleted
inserted
replaced
| 4:ce2b6dde4c10 | 5:125e599b1217 |
|---|---|
| 14 <html lang="en"> | 14 <html lang="en"> |
| 15 <head> | 15 <head> |
| 16 <meta charset="UTF-8"> | 16 <meta charset="UTF-8"> |
| 17 <meta name="viewport" content="width=device-width, initial-scale=1"> | 17 <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 18 <title>{{TITLE}}</title> | 18 <title>{{TITLE}}</title> |
| 19 <link rel="stylesheet" href="{{CSS}}"> | 19 <link rel="stylesheet" href="/x.css"> |
| 20 </head> | 20 </head> |
| 21 <body> | 21 <body> |
| 22 <nav>{{NAV}}</nav> | 22 <nav>{{NAV}}</nav> |
| 23 <header><h1>{{SITE_TITLE}}</h1></header> | 23 <header><h1>{{PAGE_TITLE}}</h1></header> |
| 24 <main>{{CONTENT}}</main> | 24 <main>{{CONTENT}}</main> |
| 25 <footer> | 25 <footer><p>{{FOOTER_TEXT}}</p></footer> |
| 26 <p>{{FOOTER_TEXT}}</p> | |
| 27 <p>Built with <a href="https://hg.reactionary.software/qwb">qwb</a></p> | |
| 28 </footer> | |
| 29 </body> | 26 </body> |
| 30 </html>` | 27 </html>` |
| 31 | 28 |
| 32 type config struct { | 29 type config struct { |
| 33 headertext string | 30 SiteTitle, FooterText string |
| 34 footertext string | 31 } |
| 35 cssfile string | 32 |
| 36 tplfile string | 33 type sect struct { |
| 37 } | |
| 38 | |
| 39 type section struct { | |
| 40 title string | 34 title string |
| 41 index string | 35 |
| 36 href string | |
| 37 | |
| 42 pages []page | 38 pages []page |
| 39 | |
| 40 hasIndex bool | |
| 41 | |
| 42 children []sect | |
| 43 } | 43 } |
| 44 | 44 |
| 45 type page struct { | 45 type page struct { |
| 46 title string | 46 title string |
| 47 path string | 47 href string |
| 48 } | 48 } |
| 49 | 49 |
| 50 func ParseINI(r io.Reader) (map[string]map[string]string, error) { | 50 func parseini(r io.Reader) map[string]string { |
| 51 res := make(map[string]map[string]string) | 51 res := make(map[string]string) |
| 52 sec := "default" | 52 sc := bufio.NewScanner(r) |
| 53 scanner := bufio.NewScanner(r) | 53 for sc.Scan() { |
| 54 for scanner.Scan() { | 54 line := strings.TrimSpace(sc.Text()) |
| 55 line := strings.TrimSpace(scanner.Text()) | 55 if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { |
| 56 if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { | |
| 57 continue | 56 continue |
| 58 } | 57 } |
| 59 if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { | |
| 60 sec = line[1 : len(line)-1] | |
| 61 if res[sec] == nil { | |
| 62 res[sec] = make(map[string]string) | |
| 63 } | |
| 64 continue | |
| 65 } | |
| 66 if k, v, ok := strings.Cut(line, "="); ok { | 58 if k, v, ok := strings.Cut(line, "="); ok { |
| 67 if res[sec] == nil { | 59 res[strings.TrimSpace(k)] = strings.TrimSpace(v) |
| 68 res[sec] = make(map[string]string) | 60 } |
| 69 } | 61 } |
| 70 res[sec][strings.TrimSpace(k)] = strings.TrimSpace(v) | 62 return res |
| 71 } | 63 } |
| 72 } | 64 |
| 73 return res, scanner.Err() | 65 func loadcfg(src string) config { |
| 74 } | 66 cfg := config{SiteTitle: "My Site", FooterText: "© 2026"} |
| 75 | |
| 76 func loadconfig(src string) config { | |
| 77 cfg := config{ | |
| 78 headertext: "My Site", | |
| 79 footertext: "© Me", | |
| 80 cssfile: "/style.css", | |
| 81 } | |
| 82 f, err := os.Open(filepath.Join(src, "qwb.ini")) | 67 f, err := os.Open(filepath.Join(src, "qwb.ini")) |
| 83 if err != nil { | 68 if err == nil { |
| 84 return cfg | 69 defer f.Close() |
| 85 } | 70 ini := parseini(f) |
| 86 defer f.Close() | 71 if v, ok := ini["SiteTitle"]; ok { |
| 87 ini, err := ParseINI(f) | 72 cfg.SiteTitle = v |
| 88 if err != nil { | 73 } |
| 89 return cfg | 74 if v, ok := ini["FooterText"]; ok { |
| 90 } | 75 cfg.FooterText = v |
| 91 s := ini["site"] | 76 } |
| 92 set := func(key string, target *string) { | 77 } |
| 93 if v, ok := s[key]; ok { | |
| 94 *target = v | |
| 95 } | |
| 96 } | |
| 97 set("header", &cfg.headertext) | |
| 98 set("footer", &cfg.footertext) | |
| 99 set("style", &cfg.cssfile) | |
| 100 set("template", &cfg.tplfile) | |
| 101 return cfg | 78 return cfg |
| 102 } | 79 } |
| 103 | 80 |
| 104 func loadtemplate(cfg config) string { | 81 func gentitle(name string) string { |
| 105 if cfg.tplfile == "" { | |
| 106 return tpldefault | |
| 107 } | |
| 108 b, err := os.ReadFile(cfg.tplfile) | |
| 109 if err != nil { | |
| 110 fmt.Fprintf(os.Stderr, "cannot read template %s: %v", cfg.tplfile, err) | |
| 111 return tpldefault | |
| 112 } | |
| 113 return string(b) | |
| 114 } | |
| 115 | |
| 116 func collectsections(root, siteTitle string) (section, []section) { | |
| 117 var subs []section | |
| 118 root_ := section{title: siteTitle} | |
| 119 entries, _ := os.ReadDir(root) | |
| 120 for _, e := range entries { | |
| 121 full := filepath.Join(root, e.Name()) | |
| 122 if e.IsDir() { | |
| 123 s := scansection(full, root) | |
| 124 if s.index != "" || len(s.pages) > 0 { | |
| 125 subs = append(subs, s) | |
| 126 } | |
| 127 continue | |
| 128 } | |
| 129 if !strings.HasSuffix(e.Name(), ".md") { | |
| 130 continue | |
| 131 } | |
| 132 if e.Name() == "index.md" { | |
| 133 root_.index = "/index.html" | |
| 134 } else { | |
| 135 rel, _ := filepath.Rel(root, full) | |
| 136 root_.pages = append(root_.pages, page{ | |
| 137 title: titlefromname(e.Name()), | |
| 138 path: "/" + strings.TrimSuffix(rel, ".md") + ".html", | |
| 139 }) | |
| 140 } | |
| 141 } | |
| 142 return root_, subs | |
| 143 } | |
| 144 | |
| 145 func scansection(dir, root string) section { | |
| 146 s := section{title: titlefromname(filepath.Base(dir))} | |
| 147 entries, _ := os.ReadDir(dir) | |
| 148 for _, e := range entries { | |
| 149 if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { | |
| 150 continue | |
| 151 } | |
| 152 rel, _ := filepath.Rel(root, filepath.Join(dir, e.Name())) | |
| 153 htmlpath := "/" + strings.TrimSuffix(rel, ".md") + ".html" | |
| 154 if e.Name() == "index.md" { | |
| 155 s.index = htmlpath | |
| 156 } else { | |
| 157 s.pages = append(s.pages, page{titlefromname(e.Name()), htmlpath}) | |
| 158 } | |
| 159 } | |
| 160 return s | |
| 161 } | |
| 162 | |
| 163 func navlink(b *strings.Builder, p page, cur string) { | |
| 164 if p.path == cur { | |
| 165 fmt.Fprintf(b, "<li><b><a href=\"%s\">%s</a></b></li>\n", p.path, p.title) | |
| 166 } else { | |
| 167 fmt.Fprintf(b, "<li><a href=\"%s\">%s</a></li>\n", p.path, p.title) | |
| 168 } | |
| 169 } | |
| 170 | |
| 171 func buildnav(root section, subs []section, cur string) string { | |
| 172 var b strings.Builder | |
| 173 b.WriteString("<ul>\n") | |
| 174 if root.index != "" { | |
| 175 navlink(&b, page{"Home", root.index}, cur) | |
| 176 } | |
| 177 for _, p := range root.pages { | |
| 178 navlink(&b, p, cur) | |
| 179 } | |
| 180 for _, s := range subs { | |
| 181 link := s.index | |
| 182 if link == "" && len(s.pages) > 0 { | |
| 183 link = s.pages[0].path | |
| 184 } | |
| 185 if link == "" { | |
| 186 continue | |
| 187 } | |
| 188 navlink(&b, page{s.title, link}, cur) | |
| 189 } | |
| 190 b.WriteString("</ul>\n") | |
| 191 for _, s := range subs { | |
| 192 if !sectioncontains(s, cur) { | |
| 193 continue | |
| 194 } | |
| 195 total := len(s.pages) | |
| 196 if s.index != "" { | |
| 197 total++ | |
| 198 } | |
| 199 b.WriteString("<ul>\n") | |
| 200 if s.index != "" { | |
| 201 navlink(&b, page{"Index", s.index}, cur) | |
| 202 } | |
| 203 for _, p := range s.pages { | |
| 204 navlink(&b, p, cur) | |
| 205 } | |
| 206 b.WriteString("</ul>\n") | |
| 207 break | |
| 208 } | |
| 209 return b.String() | |
| 210 } | |
| 211 | |
| 212 func sectioncontains(s section, cur string) bool { | |
| 213 if s.index == cur { | |
| 214 return true | |
| 215 } | |
| 216 for _, p := range s.pages { | |
| 217 if p.path == cur { | |
| 218 return true | |
| 219 } | |
| 220 } | |
| 221 return false | |
| 222 } | |
| 223 | |
| 224 func extracth1(html string) (title, rest string) { | |
| 225 start := strings.Index(html, "<h1") | |
| 226 if start == -1 { | |
| 227 return "", html | |
| 228 } | |
| 229 close := strings.Index(html[start:], ">") | |
| 230 if close == -1 { | |
| 231 return "", html | |
| 232 } | |
| 233 content := start + close + 1 | |
| 234 end := strings.Index(html[content:], "</h1>") | |
| 235 if end == -1 { | |
| 236 return "", html | |
| 237 } | |
| 238 title = html[content : content+end] | |
| 239 rest = html[:start] + html[content+end+len("</h1>"):] | |
| 240 return | |
| 241 } | |
| 242 | |
| 243 func mdtohtml(path string) (string, error) { | |
| 244 cmd := exec.Command("lowdown", "-T", "html", "--html-no-skiphtml", "--html-no-escapehtml") | |
| 245 f, err := os.Open(path) | |
| 246 if err != nil { | |
| 247 return "", err | |
| 248 } | |
| 249 defer f.Close() | |
| 250 var buf strings.Builder | |
| 251 cmd.Stdin = f | |
| 252 cmd.Stdout = &buf | |
| 253 cmd.Stderr = os.Stderr | |
| 254 if err := cmd.Run(); err != nil { | |
| 255 return "", err | |
| 256 } | |
| 257 return buf.String(), nil | |
| 258 } | |
| 259 | |
| 260 func titlefromname(name string) string { | |
| 261 name = strings.TrimSuffix(name, ".md") | 82 name = strings.TrimSuffix(name, ".md") |
| 262 name = strings.ReplaceAll(name, "-", " ") | 83 name = strings.ReplaceAll(name, "-", " ") |
| 263 if len(name) > 0 { | 84 if len(name) > 0 { |
| 264 name = strings.ToUpper(name[:1]) + name[1:] | 85 name = strings.ToUpper(name[:1]) + name[1:] |
| 265 } | 86 } |
| 266 return name | 87 return name |
| 267 } | 88 } |
| 268 | 89 |
| 269 func fixlinks(s string) string { | 90 func md2html(path string) (string, error) { |
| 270 return strings.NewReplacer( | 91 cmd := exec.Command("lowdown", "-Thtml", "--html-no-skiphtml") |
| 271 ".md)", ".html)", | 92 f, err := os.Open(path) |
| 272 ".md\"", ".html\"", | |
| 273 ".md'", ".html'", | |
| 274 ".md#", ".html#", | |
| 275 ".md>", ".html>", | |
| 276 ".md ", ".html ", | |
| 277 ".md,", ".html,", | |
| 278 ).Replace(s) | |
| 279 } | |
| 280 | |
| 281 func copyfile(src, dst string) error { | |
| 282 in, err := os.Open(src) | |
| 283 if err != nil { | 93 if err != nil { |
| 284 return err | 94 return "", err |
| 285 } | 95 } |
| 286 defer in.Close() | 96 defer f.Close() |
| 287 out, err := os.Create(dst) | 97 var buf strings.Builder |
| 98 cmd.Stdin, cmd.Stdout = f, &buf | |
| 99 if err := cmd.Run(); err != nil { | |
| 100 return "", err | |
| 101 } | |
| 102 html := buf.String() | |
| 103 | |
| 104 if trimmed := strings.TrimSpace(html); strings.HasPrefix(trimmed, "<h1") { | |
| 105 if end := strings.Index(trimmed, "</h1>"); end >= 0 { | |
| 106 html = strings.TrimSpace(trimmed[end+5:]) | |
| 107 } | |
| 108 } | |
| 109 return html, nil | |
| 110 } | |
| 111 | |
| 112 func mdtitle(path string) string { | |
| 113 f, err := os.Open(path) | |
| 288 if err != nil { | 114 if err != nil { |
| 289 return err | 115 return "" |
| 290 } | 116 } |
| 291 defer out.Close() | 117 defer f.Close() |
| 292 _, err = io.Copy(out, in) | 118 sc := bufio.NewScanner(f) |
| 293 return err | 119 for sc.Scan() { |
| 294 } | 120 line := strings.TrimSpace(sc.Text()) |
| 295 | 121 if strings.HasPrefix(line, "# ") { |
| 296 func main() { | 122 return strings.TrimSpace(line[2:]) |
| 297 if len(os.Args) != 3 { | 123 } |
| 298 fmt.Fprintln(os.Stderr, "usage: qwb <in> <out>") | 124 } |
| 299 os.Exit(1) | 125 return "" |
| 300 } | 126 } |
| 301 src, out := os.Args[1], os.Args[2] | 127 |
| 302 | 128 //, |
| 303 if entries, err := os.ReadDir(out); err == nil && len(entries) > 0 { | 129 |
| 304 fmt.Fprintf(os.Stderr, "qwb: %s is not empty, overwrite? [y/N] ", out) | 130 func mdnavhref(path string) (string, bool) { |
| 305 s, _ := bufio.NewReader(os.Stdin).ReadString('\n') | 131 f, err := os.Open(path) |
| 306 if strings.TrimSpace(strings.ToLower(s)) != "y" { | 132 if err != nil { |
| 307 fmt.Fprintln(os.Stderr, "aborted") | 133 return "", false |
| 308 os.Exit(1) | 134 } |
| 309 } | 135 defer f.Close() |
| 310 if err := os.RemoveAll(out); err != nil { | 136 sc := bufio.NewScanner(f) |
| 311 fmt.Fprintln(os.Stderr, err) | 137 if !sc.Scan() { |
| 312 os.Exit(1) | 138 return "", false |
| 313 } | 139 } |
| 314 } | 140 line := strings.TrimSpace(sc.Text()) |
| 315 | 141 if strings.Contains(line, "://") { |
| 316 cfg := loadconfig(src) | 142 return line, true |
| 317 tmpl := loadtemplate(cfg) | 143 } |
| 318 rootsec, subs := collectsections(src, cfg.headertext) | 144 return "", false |
| 319 | 145 } |
| 320 err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { | 146 |
| 147 func scansrc(src string) ([]sect, error) { | |
| 148 root := sect{title: "Home", href: "/index.html"} | |
| 149 | |
| 150 var walk func(dir, rel string, s *sect) error | |
| 151 walk = func(dir, rel string, s *sect) error { | |
| 152 entries, err := os.ReadDir(dir) | |
| 321 if err != nil { | 153 if err != nil { |
| 322 return err | 154 return err |
| 323 } | 155 } |
| 324 rel, _ := filepath.Rel(src, path) | 156 for _, e := range entries { |
| 325 outpath := filepath.Join(out, rel) | 157 name := e.Name() |
| 326 if d.IsDir() { | 158 childRel := filepath.Join(rel, name) |
| 327 return os.MkdirAll(outpath, 0755) | 159 childAbs := filepath.Join(dir, name) |
| 328 } | 160 if e.IsDir() { |
| 329 if !strings.HasSuffix(path, ".md") { | 161 child := sect{ |
| 330 return copyfile(path, outpath) | 162 title: gentitle(name), |
| 331 } | 163 href: "/" + filepath.ToSlash(childRel) + "/index.html", |
| 332 body, err := mdtohtml(path) | 164 } |
| 165 if err := walk(childAbs, childRel, &child); err != nil { | |
| 166 return err | |
| 167 } | |
| 168 s.children = append(s.children, child) | |
| 169 continue | |
| 170 } | |
| 171 if !strings.HasSuffix(name, ".md") { | |
| 172 continue | |
| 173 } | |
| 174 if name == "index.md" { | |
| 175 s.hasIndex = true | |
| 176 if ext, ok := mdnavhref(childAbs); ok { | |
| 177 s.href = ext | |
| 178 } | |
| 179 } else { | |
| 180 href := "/" + filepath.ToSlash(strings.TrimSuffix(childRel, ".md")+".html") | |
| 181 if ext, ok := mdnavhref(childAbs); ok { | |
| 182 href = ext | |
| 183 } | |
| 184 s.pages = append(s.pages, page{ | |
| 185 title: gentitle(name), | |
| 186 href: href, | |
| 187 }) | |
| 188 } | |
| 189 } | |
| 190 return nil | |
| 191 } | |
| 192 | |
| 193 if err := walk(src, "", &root); err != nil { | |
| 194 return nil, err | |
| 195 } | |
| 196 return []sect{root}, nil | |
| 197 } | |
| 198 | |
| 199 func findCurSect(sects []sect, cur string) *sect { | |
| 200 for i := range sects { | |
| 201 s := §s[i] | |
| 202 if s.href == cur { | |
| 203 return s | |
| 204 } | |
| 205 for _, p := range s.pages { | |
| 206 if p.href == cur { | |
| 207 return s | |
| 208 } | |
| 209 } | |
| 210 if found := findCurSect(s.children, cur); found != nil { | |
| 211 return found | |
| 212 } | |
| 213 } | |
| 214 return nil | |
| 215 } | |
| 216 | |
| 217 func navX(roots []sect, cur string) string { | |
| 218 if len(roots) == 0 { | |
| 219 return "" | |
| 220 } | |
| 221 root := &roots[0] | |
| 222 | |
| 223 var path []*sect | |
| 224 var findPath func(s *sect) bool | |
| 225 findPath = func(s *sect) bool { | |
| 226 path = append(path, s) | |
| 227 if s.href == cur { | |
| 228 return true | |
| 229 } | |
| 230 for _, p := range s.pages { | |
| 231 if p.href == cur { | |
| 232 return true | |
| 233 } | |
| 234 } | |
| 235 for i := range s.children { | |
| 236 if findPath(&s.children[i]) { | |
| 237 return true | |
| 238 } | |
| 239 } | |
| 240 path = path[:len(path)-1] | |
| 241 return false | |
| 242 } | |
| 243 if !findPath(root) { | |
| 244 return "" | |
| 245 } | |
| 246 | |
| 247 sectLI := func(b *strings.Builder, s *sect, active bool) { | |
| 248 b.WriteString(" <li>") | |
| 249 if s.hasIndex { | |
| 250 cls := "" | |
| 251 if active { | |
| 252 cls = " current" | |
| 253 } | |
| 254 b.WriteString(`<a` + cls + ` href="` + s.href + `">` + s.title + `</a>`) | |
| 255 } else { | |
| 256 b.WriteString(s.title) | |
| 257 } | |
| 258 b.WriteString("</li>\n") | |
| 259 } | |
| 260 | |
| 261 var b strings.Builder | |
| 262 | |
| 263 { | |
| 264 var active1 *sect | |
| 265 | |
| 266 if len(path) > 1 { | |
| 267 active1 = path[1] | |
| 268 } | |
| 269 b.WriteString("<ul>\n") | |
| 270 sectLI(&b, root, len(path) == 1) | |
| 271 | |
| 272 for i := range root.children { | |
| 273 c := &root.children[i] | |
| 274 sectLI(&b, c, c == active1) | |
| 275 } | |
| 276 b.WriteString("</ul>\n") | |
| 277 } | |
| 278 | |
| 279 for depth := 1; depth < len(path); depth++ { | |
| 280 s := path[depth] | |
| 281 if len(s.children) == 0 && len(s.pages) == 0 { | |
| 282 continue | |
| 283 } | |
| 284 var activeChild *sect | |
| 285 if depth+1 < len(path) { | |
| 286 activeChild = path[depth+1] | |
| 287 } | |
| 288 b.WriteString("<ul>\n") | |
| 289 for i := range s.children { | |
| 290 c := &s.children[i] | |
| 291 sectLI(&b, c, c == activeChild) | |
| 292 } | |
| 293 for _, p := range s.pages { | |
| 294 cls := "" | |
| 295 if p.href == cur { | |
| 296 cls = " current" | |
| 297 } | |
| 298 b.WriteString(` <li><a` + cls + ` href="` + p.href + `">` + p.title + `</a></li>` + "\n") | |
| 299 } | |
| 300 b.WriteString("</ul>\n") | |
| 301 } | |
| 302 | |
| 303 return b.String() | |
| 304 } | |
| 305 | |
| 306 func navY(roots []sect, cur string) string { | |
| 307 curSect := findCurSect(roots, cur) | |
| 308 | |
| 309 var isAncestor func(s *sect) bool | |
| 310 isAncestor = func(s *sect) bool { | |
| 311 if s == curSect { | |
| 312 return true | |
| 313 } | |
| 314 for i := range s.children { | |
| 315 if isAncestor(&s.children[i]) { | |
| 316 return true | |
| 317 } | |
| 318 } | |
| 319 return false | |
| 320 } | |
| 321 | |
| 322 var b strings.Builder | |
| 323 var renderSects func(sects []sect, depth int) | |
| 324 renderSects = func(sects []sect, depth int) { | |
| 325 indent := strings.Repeat(" ", depth) | |
| 326 b.WriteString(indent + "<ul>\n") | |
| 327 for i := range sects { | |
| 328 s := §s[i] | |
| 329 b.WriteString(indent + " <li>") | |
| 330 if s.hasIndex { | |
| 331 cls := "" | |
| 332 if s == curSect { | |
| 333 cls = " current" | |
| 334 } | |
| 335 b.WriteString(`<a` + cls + ` href="` + s.href + `">` + s.title + `</a>`) | |
| 336 } else { | |
| 337 b.WriteString(s.title) | |
| 338 } | |
| 339 | |
| 340 if isAncestor(s) { | |
| 341 if len(s.pages) > 0 || len(s.children) > 0 { | |
| 342 b.WriteString("\n") | |
| 343 if len(s.children) > 0 { | |
| 344 renderSects(s.children, depth+2) | |
| 345 } | |
| 346 if len(s.pages) > 0 { | |
| 347 b.WriteString(indent + " <ul>\n") | |
| 348 for _, p := range s.pages { | |
| 349 cls := "" | |
| 350 if p.href == cur { | |
| 351 cls = " current" | |
| 352 } | |
| 353 b.WriteString(indent + ` <li><a` + cls + ` href="` + p.href + `">` + p.title + `</a></li>` + "\n") | |
| 354 } | |
| 355 b.WriteString(indent + " </ul>\n") | |
| 356 } | |
| 357 b.WriteString(indent + " ") | |
| 358 } | |
| 359 } | |
| 360 b.WriteString("</li>\n") | |
| 361 } | |
| 362 b.WriteString(indent + "</ul>\n") | |
| 363 } | |
| 364 renderSects(roots, 0) | |
| 365 return b.String() | |
| 366 } | |
| 367 | |
| 368 func render(tpl, pageTitle, nav, content string, cfg config) string { | |
| 369 compositeTitle := cfg.SiteTitle + " | " + pageTitle | |
| 370 r := strings.NewReplacer( | |
| 371 "{{TITLE}}", compositeTitle, | |
| 372 "{{NAV}}", nav, | |
| 373 "{{PAGE_TITLE}}", pageTitle, | |
| 374 "{{CONTENT}}", content, | |
| 375 "{{FOOTER_TEXT}}", cfg.FooterText, | |
| 376 ) | |
| 377 return r.Replace(tpl) | |
| 378 } | |
| 379 | |
| 380 func writepage(outpath, html string) error { | |
| 381 if err := os.MkdirAll(filepath.Dir(outpath), 0755); err != nil { | |
| 382 return err | |
| 383 } | |
| 384 return os.WriteFile(outpath, []byte(html), 0644) | |
| 385 } | |
| 386 | |
| 387 func main() { | |
| 388 if len(os.Args) < 3 { | |
| 389 fmt.Println("usage: qwb <src> <out> [-x|-y]") | |
| 390 return | |
| 391 } | |
| 392 src, out := os.Args[1], os.Args[2] | |
| 393 mode := "x" | |
| 394 if len(os.Args) == 4 { | |
| 395 switch os.Args[3] { | |
| 396 case "-x": | |
| 397 mode = "x" | |
| 398 case "-y": | |
| 399 mode = "y" | |
| 400 default: | |
| 401 fmt.Println("usage: qwb <src> <out> [-x|-y]") | |
| 402 return | |
| 403 } | |
| 404 } | |
| 405 | |
| 406 cfg := loadcfg(src) | |
| 407 | |
| 408 sects, err := scansrc(src) | |
| 409 if err != nil { | |
| 410 fmt.Fprintln(os.Stderr, "scan:", err) | |
| 411 os.Exit(1) | |
| 412 } | |
| 413 | |
| 414 var process func(dir, rel string) | |
| 415 process = func(dir, rel string) { | |
| 416 entries, err := os.ReadDir(dir) | |
| 333 if err != nil { | 417 if err != nil { |
| 334 return err | 418 return |
| 335 } | 419 } |
| 336 body = fixlinks(body) | 420 for _, e := range entries { |
| 337 pageTitle, body := extracth1(body) | 421 name := e.Name() |
| 338 if pageTitle == "" { | 422 childRel := filepath.Join(rel, name) |
| 339 pageTitle = cfg.headertext | 423 childAbs := filepath.Join(dir, name) |
| 340 } | 424 if e.IsDir() { |
| 341 cur := "/" + strings.TrimSuffix(rel, ".md") + ".html" | 425 process(childAbs, childRel) |
| 342 title := cfg.headertext | 426 continue |
| 343 if filepath.Base(path) != "index.md" { | 427 } |
| 344 title = cfg.headertext + " | " + pageTitle | 428 if !strings.HasSuffix(name, ".md") { |
| 345 } | 429 |
| 346 pg := strings.ReplaceAll(tmpl, "{{TITLE}}", title) | 430 dst := filepath.Join(out, childRel) |
| 347 pg = strings.ReplaceAll(pg, "{{SITE_TITLE}}", pageTitle) | 431 _ = os.MkdirAll(filepath.Dir(dst), 0755) |
| 348 pg = strings.ReplaceAll(pg, "{{FOOTER_TEXT}}", cfg.footertext) | 432 if data, err := os.ReadFile(childAbs); err == nil { |
| 349 pg = strings.ReplaceAll(pg, "{{CSS}}", cfg.cssfile) | 433 _ = os.WriteFile(dst, data, 0644) |
| 350 pg = strings.ReplaceAll(pg, "{{NAV}}", buildnav(rootsec, subs, cur)) | 434 } |
| 351 pg = strings.ReplaceAll(pg, "{{CONTENT}}", body) | 435 continue |
| 352 outpath = strings.TrimSuffix(outpath, ".md") + ".html" | 436 } |
| 353 return os.WriteFile(outpath, []byte(pg), 0644) | 437 htmlRel := strings.TrimSuffix(childRel, ".md") + ".html" |
| 354 }) | 438 href := "/" + filepath.ToSlash(htmlRel) |
| 355 | 439 |
| 356 if err != nil { | 440 var nav string |
| 357 fmt.Fprintln(os.Stderr, err) | 441 if mode == "y" { |
| 358 os.Exit(1) | 442 nav = navY(sects, href) |
| 359 } | 443 } else { |
| 360 } | 444 nav = navX(sects, href) |
| 445 } | |
| 446 | |
| 447 pageTitle := mdtitle(childAbs) | |
| 448 if pageTitle == "" { | |
| 449 pageTitle = gentitle(name) | |
| 450 } | |
| 451 | |
| 452 content, err := md2html(childAbs) | |
| 453 if err != nil { | |
| 454 fmt.Fprintf(os.Stderr, "md2html %s: %v\n", childAbs, err) | |
| 455 content = "<p>render error</p>" | |
| 456 } | |
| 457 | |
| 458 html := render(tpldefault, pageTitle, nav, content, cfg) | |
| 459 outpath := filepath.Join(out, htmlRel) | |
| 460 if err := writepage(outpath, html); err != nil { | |
| 461 fmt.Fprintf(os.Stderr, "write %s: %v\n", outpath, err) | |
| 462 } else { | |
| 463 fmt.Println("→", outpath) | |
| 464 } | |
| 465 } | |
| 466 } | |
| 467 | |
| 468 process(src, "") | |
| 469 } |
