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>&copy; 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: "&copy; 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)
 	})