Mercurial Hosting > qwb
changeset 2:3222f88c0afe
refactored + new css
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Sat, 14 Mar 2026 12:03:52 +0500 |
| parents | 72124c0555c8 |
| children | f1209af34e3d |
| files | README.md in/x.css in/z.css qwb.go |
| diffstat | 4 files changed, 186 insertions(+), 92 deletions(-) [+] |
line wrap: on
line diff
--- a/README.md Fri Mar 13 14:25:43 2026 +0500 +++ b/README.md Sat Mar 14 12:03:52 2026 +0500 @@ -1,3 +1,32 @@ # Qwaderton's Website Builder -qwb is a very stripped fork of kew, optimized specificly for x.css \ No newline at end of file +qwb is a very stripped fork of kew, optimized specificly for x.css + +## conf + +qwb reads `qwb.ini` file in first argument directory +if you run: +``` +$ qwb in/ out/ +``` + +qwb will read configuration from in/qwb.ini + +### ref + +\[site\] block +**header:** string +**footer:** string +**style:** relative to webroot path +**template:** relative to pwd or absolute path to html file + +## template + +it should contain these placeholders + +{{TITLE}} +{{SITE_TITLE}} +{{FOOTER_TEXT}} +{{CSS}} +{{NAV}} +{{CONTENT}} \ No newline at end of file
--- a/in/x.css Fri Mar 13 14:25:43 2026 +0500 +++ b/in/x.css Sat Mar 14 12:03:52 2026 +0500 @@ -39,24 +39,24 @@ padding-top: 0.5rem; } -footer p { - float: left; +footer p:first-child:not(:last-child) { + display: inline-block; width: 50%; } footer p:nth-child(2) { + float: right; text-align: right; } nav ul { list-style: none; + border-bottom: 1px solid #eee; padding-bottom: 0.5rem; - display: inline-block; - border-bottom: 1px solid #eee; } nav ul:last-child { - float: right; + text-align: right; } nav li { @@ -129,15 +129,13 @@ } } -/* Dark mode support for modern browsers (ignored by NetSurf/Chawan) */ +/* Dark mode support */ @media (prefers-color-scheme: dark) { body { background: #1a1a1a; color: #eee; } h1, h2, h3 { color: #fff; } a { color: #3291ff; } blockquote { border-color: #444; } - th, td { border-color: #333; } + th, td, ul, footer { border-color: #333 !important; } button { background: #eee; color: #111; } input, textarea, button { background-color: #222; color: #ddd; } - nav ul:last-child, footer { border-color: #333; } } -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/in/z.css Sat Mar 14 12:03:52 2026 +0500 @@ -0,0 +1,8 @@ +nav ul { + padding-bottom: 0.5rem; + display: inline-block; +} + +nav ul:last-child { + float: right; +}
--- a/qwb.go Fri Mar 13 14:25:43 2026 +0500 +++ b/qwb.go Sat Mar 14 12:03:52 2026 +0500 @@ -10,32 +10,36 @@ "strings" ) -const siteTitle = "My Site" - -const tmpl = `<!DOCTYPE html> +const tpldefault = `<!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"> +<link rel="stylesheet" href="{{CSS}}"> </head> <body> -<header><h1>` + siteTitle + `</h1></header> <nav>{{NAV}}</nav> +<header><h1>{{SITE_TITLE}}</h1></header> <main>{{CONTENT}}</main> <footer> -<p>` + siteTitle + `</p> -<p>© Built with <a href="https://hg.reactionary.software/qwb">qwb</a></p> +<p>{{FOOTER_TEXT}}</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 config struct { + headertext string + footertext string + cssfile string + tplfile string +} + type section struct { title string - index string // /section/index.html, or "" if none - pages []page // non-index .md files within this section + index string + pages []page } type page struct { @@ -43,58 +47,120 @@ 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 +func ParseINI(r io.Reader) (map[string]map[string]string, error) { + res := make(map[string]map[string]string) + sec := "default" + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + sec = line[1 : len(line)-1] + if res[sec] == nil { + res[sec] = make(map[string]string) + } + continue + } + if k, v, ok := strings.Cut(line, "="); ok { + if res[sec] == nil { + res[sec] = make(map[string]string) + } + res[sec][strings.TrimSpace(k)] = strings.TrimSpace(v) + } + } + return res, scanner.Err() +} +func loadconfig(src string) config { + cfg := config{ + headertext: "My Site", + footertext: "© Me", + cssfile: "/style.css", + } + f, err := os.Open(filepath.Join(src, "qwb.ini")) + if err != nil { + return cfg + } + defer f.Close() + ini, err := ParseINI(f) + if err != nil { + return cfg + } + s := ini["site"] + set := func(key string, target *string) { + if v, ok := s[key]; ok { + *target = v + } + } + set("header", &cfg.headertext) + set("footer", &cfg.footertext) + set("style", &cfg.cssfile) + set("template", &cfg.tplfile) + return cfg +} + +func loadtemplate(cfg config) string { + if cfg.tplfile == "" { + return tpldefault + } + b, err := os.ReadFile(cfg.tplfile) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot read template %s: %v", cfg.tplfile, err) + return tpldefault + } + return string(b) +} + +func collectsections(root, siteTitle string) (section, []section) { + var subs []section + root_ := section{title: siteTitle} entries, _ := os.ReadDir(root) for _, e := range entries { full := filepath.Join(root, e.Name()) if e.IsDir() { - s := scanSection(full, root) + 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 + root_.index = "/index.html" } else { - s.pages = append(s.pages, page{titleFromName(e.Name()), htmlPath}) + rel, _ := filepath.Rel(root, full) + root_.pages = append(root_.pages, page{ + title: titlefromname(e.Name()), + path: "/" + strings.TrimSuffix(rel, ".md") + ".html", + }) + } + } + return root_, subs +} + +func scansection(dir, root string) section { + s := section{title: titlefromname(filepath.Base(dir))} + entries, _ := os.ReadDir(dir) + for _, e := range entries { + if e.IsDir() || !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) { +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 { @@ -102,18 +168,14 @@ } } -// 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 { +func buildnav(root section, subs []section, cur string) string { var b strings.Builder - b.WriteString("<ul>\n") - if rootSec.index != "" { - navLink(&b, page{"Home", rootSec.index}, cur) + if root.index != "" { + navlink(&b, page{"Home", root.index}, cur) } - for _, p := range rootSec.pages { - navLink(&b, p, cur) + for _, p := range root.pages { + navlink(&b, p, cur) } for _, s := range subs { link := s.index @@ -123,16 +185,13 @@ if link == "" { continue } - navLink(&b, page{s.title, link}, cur) + 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) { + 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++ @@ -142,19 +201,18 @@ } b.WriteString("<ul>\n") if s.index != "" { - navLink(&b, page{"Index", s.index}, cur) + navlink(&b, page{"Index", s.index}, cur) } for _, p := range s.pages { - navLink(&b, p, cur) + navlink(&b, p, cur) } b.WriteString("</ul>\n") break } - return b.String() } -func sectionContains(s section, cur string) bool { +func sectioncontains(s section, cur string) bool { if s.index == cur { return true } @@ -166,7 +224,7 @@ return false } -func mdToHTML(path string) (string, error) { +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 { @@ -183,7 +241,7 @@ return buf.String(), nil } -func titleFromName(name string) string { +func titlefromname(name string) string { name = strings.TrimSuffix(name, ".md") name = strings.ReplaceAll(name, "-", " ") if len(name) > 0 { @@ -192,16 +250,19 @@ return name } -func fixLinks(s string) string { +func fixlinks(s string) string { return strings.NewReplacer( - ".md)", ".html)", ".md\"", ".html\"", - ".md'", ".html'", ".md#", ".html#", - ".md>", ".html>", ".md ", ".html ", + ".md)", ".html)", + ".md\"", ".html\"", + ".md'", ".html'", + ".md#", ".html#", + ".md>", ".html>", + ".md ", ".html ", ".md,", ".html,", ).Replace(s) } -func copyFile(src, dst string) error { +func copyfile(src, dst string) error { in, err := os.Open(src) if err != nil { return err @@ -236,7 +297,9 @@ } } - rootSec, subs := collectSections(src) + cfg := loadconfig(src) + tmpl := loadtemplate(cfg) + rootsec, subs := collectsections(src, cfg.headertext) err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { if err != nil { @@ -244,32 +307,28 @@ } 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) + return copyfile(path, outpath) } - - body, err := mdToHTML(path) + body, err := mdtohtml(path) if err != nil { return err } - body = fixLinks(body) - + body = fixlinks(body) cur := "/" + strings.TrimSuffix(rel, ".md") + ".html" - - title := siteTitle + title := cfg.headertext if filepath.Base(path) != "index.md" { - title = siteTitle + " | " + titleFromName(filepath.Base(path)) + title = cfg.headertext + " | " + titlefromname(filepath.Base(path)) } - - pg := strings.ReplaceAll(tmpl, "{{TITLE}}", title) - pg = strings.ReplaceAll(pg, "{{NAV}}", buildNav(rootSec, subs, cur)) + pg:= strings.ReplaceAll(tmpl, "{{TITLE}}", title) + pg = strings.ReplaceAll(pg, "{{SITE_TITLE}}", cfg.headertext) + pg = strings.ReplaceAll(pg, "{{FOOTER_TEXT}}", cfg.footertext) + pg = strings.ReplaceAll(pg, "{{CSS}}", cfg.cssfile) + 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) })
