changeset 5:125e599b1217

agbubu
author Atarwn Gard <a@qwa.su>
date Tue, 17 Mar 2026 22:18:02 +0500
parents ce2b6dde4c10
children bd0d3a189f5b
files in/index.md in/project/about.md in/project/index.md in/qwb.ini in/x.css in/z.css out/index.html out/project/about.html out/project/docs/about.html out/project/docs/authors.html out/project/docs/index.html out/project/index.html out/qwb.ini out/x.css out/z.css qwb.go src/index.md src/project/about.md src/project/docs/authors.md src/project/docs/index.md src/project/index.md src/qwb.ini src/x.css src/z.css
diffstat 24 files changed, 865 insertions(+), 447 deletions(-) [+]
line wrap: on
line diff
--- a/in/index.md	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Hello world
\ No newline at end of file
--- a/in/project/about.md	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-# About my Project
-
-Information about my project
\ No newline at end of file
--- a/in/project/index.md	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-# My Project
-
-My project
\ No newline at end of file
--- a/in/qwb.ini	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-[site]
-header=My Site
-footer=&copy; Me
-style=/x.css
\ No newline at end of file
--- a/in/x.css	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-/* 1. The Reset: Neutralizing browser quirks across engines */
-* { 
-    box-sizing: border-box; 
-    margin: 0; 
-    padding: 0;
-}
-
-/* 2. Typography: Manrope with system fallbacks for lightweight browsers */
-@font-face {
-    font-family: 'Manrope';
-    src: url('https://qwaderton.org/Manrope-VariableFont_wght.woff2') format('woff2-variations');
-    font-weight: 100 800;
-}
-
-html {
-    font-size: 100%;
-    -webkit-text-size-adjust: 100%;
-}
-
-body {
-    font-family: 'Manrope', system-ui, -apple-system, sans-serif;
-    line-height: 1.6;
-    color: #333;
-    background: #fff;
-    max-width: 45rem; /* The "Golden Width" for readability */
-    margin: 2rem auto;
-    padding: 0 1rem;
-    text-rendering: optimizeLegibility;
-}
-
-/* 3. Semantic Layout: No classes needed */
-header, nav, main, section, footer {
-    display: block; /* Fix for older browsers */
-    margin-bottom: 2rem;
-}
-
-footer {
-    border-top: 1px solid #eee;
-    padding-top: 0.5rem;
-}
-
-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;
-}
-
-nav ul:last-child {
-    text-align: right;
-}
-
-nav li {
-    display: inline;
-    margin-right: 1rem;
-}
-
-/* 4. Elements */
-h1, h2, h3, h4 {
-    line-height: 1.2;
-    margin-top: 2rem;
-    margin-bottom: 1rem;
-    font-weight: 700;
-}
-
-a { color: #0070f3; text-decoration: none; }
-a:hover { text-decoration: underline; }
-
-p, blockquote, ul, ol, dl, table, pre {
-    margin-bottom: 1.5rem;
-}
-
-blockquote {
-    border-left: 4px solid #eee;
-    padding-left: 1rem;
-    font-style: italic;
-}
-
-img, video {
-    max-width: 100%;
-    height: auto;
-}
-
-/* 5. Tables & Forms: Standardized for basic engines */
-table {
-    width: 100%;
-    border-collapse: collapse;
-}
-
-th, td {
-    padding: 0.5rem;
-    text-align: left;
-    border-bottom: 1px solid #eee;
-}
-
-input, textarea, button, select {
-    font-family: inherit;
-    font-size: 1rem;
-    padding: 0.5rem;
-    margin-bottom: 1rem;
-    display: block;
-}
-
-input[type="checkbox"],input[type="radio"] {
-    display: inline
-}
-
-button, select {
-    width: auto;
-    cursor: pointer;
-    background: #333;
-    color: #fff;
-    border: none;
-    border-radius: 4px;
-}
-
-@media (max-width: 46rem) {
-    li {
-        margin-left: 1rem;
-    }
-}
-
-/* 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, ul, footer { border-color: #333 !important; }
-    button { background: #eee; color: #111; }
-    input, textarea, button { background-color: #222; color: #ddd; }
-}
--- a/in/z.css	Sat Mar 14 14:01:51 2026 +0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-nav ul {
-    padding-bottom: 0.5rem;
-    display: inline-block;
-}
-
-nav ul:last-child {
-    float: right;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/index.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>My Site | Index</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a current href="/index.html">Home</a></li>
+ <li><a href="/project/index.html">Project</a></li>
+</ul>
+</nav>
+<header><h1>Index</h1></header>
+<main><p>Hello world</p>
+</main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/project/about.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>My Site | About my Project</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a href="/index.html">Home</a></li>
+ <li><a current href="/project/index.html">Project</a></li>
+</ul>
+<ul>
+ <li><a href="/project/docs/index.html">Docs</a></li>
+ <li><a current href="/project/about.html">About</a></li>
+</ul>
+</nav>
+<header><h1>About my Project</h1></header>
+<main><p>Information about my project</p></main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/project/docs/about.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>About</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a href="/index.html">Home</a></li>
+ <li><a current href="/project/index.html">Project</a>
+ <ul>
+  <li><a current href="/project/about.html">About</a></li>
+ </ul>
+ </li>
+</ul>
+</nav>
+<header><h1>My Site</h1></header>
+<main><h1 id="about-my-project">About my Project</h1>
+<p>Information about my project</p>
+</main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/project/docs/authors.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>My Site | Authors</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a href="/index.html">Home</a></li>
+ <li><a current href="/project/index.html">Project</a></li>
+</ul>
+<ul>
+ <li><a current href="/project/docs/index.html">Docs</a></li>
+ <li><a href="/project/about.html">About</a></li>
+</ul>
+<ul>
+ <li><a current href="/project/docs/authors.html">Authors</a></li>
+</ul>
+</nav>
+<header><h1>Authors</h1></header>
+<main><p>Authors of the project</p></main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/project/docs/index.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>My Site | My Project Documentation</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a href="/index.html">Home</a></li>
+ <li><a current href="/project/index.html">Project</a></li>
+</ul>
+<ul>
+ <li><a current href="/project/docs/index.html">Docs</a></li>
+ <li><a href="/project/about.html">About</a></li>
+</ul>
+<ul>
+ <li><a href="/project/docs/authors.html">Authors</a></li>
+</ul>
+</nav>
+<header><h1>My Project Documentation</h1></header>
+<main><p>y</p></main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/project/index.html	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>My Site | My Project</title>
+<link rel="stylesheet" href="/x.css"> 
+</head>
+<body>
+<nav><ul>
+ <li><a href="/index.html">Home</a></li>
+ <li><a current href="/project/index.html">Project</a></li>
+</ul>
+<ul>
+ <li><a href="/project/docs/index.html">Docs</a></li>
+ <li><a href="/project/about.html">About</a></li>
+</ul>
+</nav>
+<header><h1>My Project</h1></header>
+<main><p>My project</p></main>
+<footer><p>&copy; Me</p></footer>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/qwb.ini	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,4 @@
+[site]
+SiteTitle=My Site
+FooterText=&copy; Me
+style=/x.css
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/x.css	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,145 @@
+/* 1. The Reset: Neutralizing browser quirks across engines */
+* { 
+    box-sizing: border-box; 
+    margin: 0; 
+    padding: 0;
+}
+
+/* 2. Typography: Manrope with system fallbacks for lightweight browsers */
+@font-face {
+    font-family: 'Manrope';
+    src: url('https://qwaderton.org/Manrope-VariableFont_wght.woff2') format('woff2-variations');
+    font-weight: 100 800;
+}
+
+html {
+    font-size: 100%;
+    -webkit-text-size-adjust: 100%;
+}
+
+body {
+    font-family: 'Manrope', system-ui, -apple-system, sans-serif;
+    line-height: 1.6;
+    color: #333;
+    background: #fff;
+    max-width: 45rem; /* The "Golden Width" for readability */
+    margin: 2rem auto;
+    padding: 0 1rem;
+    text-rendering: optimizeLegibility;
+}
+
+/* 3. Semantic Layout: No classes needed */
+header, nav, main, section, footer {
+    display: block; /* Fix for older browsers */
+    margin-bottom: 2rem;
+}
+
+footer {
+    border-top: 1px solid #eee;
+    padding-top: 0.5rem;
+}
+
+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;
+}
+
+nav ul:last-child:not(:first-child) {
+    text-align: right;
+}
+
+nav li {
+    display: inline;
+    margin-right: 1rem;
+}
+
+nav a[current] {
+    font-weight: bold;
+}
+
+/* 4. Elements */
+h1, h2, h3, h4 {
+    line-height: 1.2;
+    margin-top: 2rem;
+    margin-bottom: 1rem;
+    font-weight: 700;
+}
+
+a { color: #0070f3; text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+p, blockquote, ul, ol, dl, table, pre {
+    margin-bottom: 1.5rem;
+}
+
+blockquote {
+    border-left: 4px solid #eee;
+    padding-left: 1rem;
+    font-style: italic;
+}
+
+img, video {
+    max-width: 100%;
+    height: auto;
+}
+
+/* 5. Tables & Forms: Standardized for basic engines */
+table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+th, td {
+    padding: 0.5rem;
+    text-align: left;
+    border-bottom: 1px solid #eee;
+}
+
+input, textarea, button, select {
+    font-family: inherit;
+    font-size: 1rem;
+    padding: 0.5rem;
+    margin-bottom: 1rem;
+    display: block;
+}
+
+input[type="checkbox"],input[type="radio"] {
+    display: inline
+}
+
+button, select {
+    width: auto;
+    cursor: pointer;
+    background: #333;
+    color: #fff;
+    border: none;
+    border-radius: 4px;
+}
+
+@media (max-width: 46rem) {
+    li {
+        margin-left: 1rem;
+    }
+}
+
+/* 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, ul, footer { border-color: #333 !important; }
+    button { background: #eee; color: #111; }
+    input, textarea, button { background-color: #222; color: #ddd; }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/out/z.css	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,8 @@
+nav ul {
+    padding-bottom: 0.5rem;
+    display: inline-block;
+}
+
+nav ul:last-child {
+    float: right;
+}
--- a/qwb.go	Sat Mar 14 14:01:51 2026 +0500
+++ b/qwb.go	Tue Mar 17 22:18:02 2026 +0500
@@ -16,248 +16,69 @@
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>{{TITLE}}</title>
-<link rel="stylesheet" href="{{CSS}}">
+<link rel="stylesheet" href="/x.css"> 
 </head>
 <body>
 <nav>{{NAV}}</nav>
-<header><h1>{{SITE_TITLE}}</h1></header>
+<header><h1>{{PAGE_TITLE}}</h1></header>
 <main>{{CONTENT}}</main>
-<footer>
-<p>{{FOOTER_TEXT}}</p>
-<p>Built with <a href="https://hg.reactionary.software/qwb">qwb</a></p>
-</footer>
+<footer><p>{{FOOTER_TEXT}}</p></footer>
 </body>
 </html>`
 
 type config struct {
-	headertext string
-	footertext string
-	cssfile    string
-	tplfile    string
+	SiteTitle, FooterText string
 }
 
-type section struct {
+type sect struct {
 	title string
-	index string
+
+	href string
+
 	pages []page
+
+	hasIndex bool
+
+	children []sect
 }
 
 type page struct {
 	title string
-	path  string
+	href  string
 }
 
-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)
-			}
+func parseini(r io.Reader) map[string]string {
+	res := make(map[string]string)
+	sc := bufio.NewScanner(r)
+	for sc.Scan() {
+		line := strings.TrimSpace(sc.Text())
+		if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") {
 			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)
+			res[strings.TrimSpace(k)] = strings.TrimSpace(v)
 		}
 	}
-	return res, scanner.Err()
+	return res
 }
 
-func loadconfig(src string) config {
-	cfg := config{
-		headertext: "My Site",
-		footertext: "&copy; Me",
-		cssfile:    "/style.css",
-	}
+func loadcfg(src string) config {
+	cfg := config{SiteTitle: "My Site", FooterText: "© 2026"}
 	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
+	if err == nil {
+		defer f.Close()
+		ini := parseini(f)
+		if v, ok := ini["SiteTitle"]; ok {
+			cfg.SiteTitle = v
+		}
+		if v, ok := ini["FooterText"]; ok {
+			cfg.FooterText = 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)
-			if s.index != "" || len(s.pages) > 0 {
-				subs = append(subs, s)
-			}
-			continue
-		}
-		if !strings.HasSuffix(e.Name(), ".md") {
-			continue
-		}
-		if e.Name() == "index.md" {
-			root_.index = "/index.html"
-		} else {
-			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) {
-	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)
-	}
-}
-
-func buildnav(root section, subs []section, cur string) string {
-	var b strings.Builder
-	b.WriteString("<ul>\n")
-	if root.index != "" {
-		navlink(&b, page{"Home", root.index}, cur)
-	}
-	for _, p := range root.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")
-	for _, s := range subs {
-		if !sectioncontains(s, cur) {
-			continue
-		}
-		total := len(s.pages)
-		if s.index != "" {
-			total++
-		}
-		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 extracth1(html string) (title, rest string) {
-	start := strings.Index(html, "<h1")
-	if start == -1 {
-		return "", html
-	}
-	close := strings.Index(html[start:], ">")
-	if close == -1 {
-		return "", html
-	}
-	content := start + close + 1
-	end := strings.Index(html[content:], "</h1>")
-	if end == -1 {
-		return "", html
-	}
-	title = html[content : content+end]
-	rest = html[:start] + html[content+end+len("</h1>"):]
-	return
-}
-
-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 {
+func gentitle(name string) string {
 	name = strings.TrimSuffix(name, ".md")
 	name = strings.ReplaceAll(name, "-", " ")
 	if len(name) > 0 {
@@ -266,95 +87,383 @@
 	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 md2html(path string) (string, error) {
+	cmd := exec.Command("lowdown", "-Thtml", "--html-no-skiphtml")
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+	var buf strings.Builder
+	cmd.Stdin, cmd.Stdout = f, &buf
+	if err := cmd.Run(); err != nil {
+		return "", err
+	}
+	html := buf.String()
+
+	if trimmed := strings.TrimSpace(html); strings.HasPrefix(trimmed, "<h1") {
+		if end := strings.Index(trimmed, "</h1>"); end >= 0 {
+			html = strings.TrimSpace(trimmed[end+5:])
+		}
+	}
+	return html, nil
+}
+
+func mdtitle(path string) string {
+	f, err := os.Open(path)
+	if err != nil {
+		return ""
+	}
+	defer f.Close()
+	sc := bufio.NewScanner(f)
+	for sc.Scan() {
+		line := strings.TrimSpace(sc.Text())
+		if strings.HasPrefix(line, "# ") {
+			return strings.TrimSpace(line[2:])
+		}
+	}
+	return ""
+}
+
+//,
+
+func mdnavhref(path string) (string, bool) {
+	f, err := os.Open(path)
+	if err != nil {
+		return "", false
+	}
+	defer f.Close()
+	sc := bufio.NewScanner(f)
+	if !sc.Scan() {
+		return "", false
+	}
+	line := strings.TrimSpace(sc.Text())
+	if strings.Contains(line, "://") {
+		return line, true
+	}
+	return "", false
+}
+
+func scansrc(src string) ([]sect, error) {
+	root := sect{title: "Home", href: "/index.html"}
+
+	var walk func(dir, rel string, s *sect) error
+	walk = func(dir, rel string, s *sect) error {
+		entries, err := os.ReadDir(dir)
+		if err != nil {
+			return err
+		}
+		for _, e := range entries {
+			name := e.Name()
+			childRel := filepath.Join(rel, name)
+			childAbs := filepath.Join(dir, name)
+			if e.IsDir() {
+				child := sect{
+					title: gentitle(name),
+					href:  "/" + filepath.ToSlash(childRel) + "/index.html",
+				}
+				if err := walk(childAbs, childRel, &child); err != nil {
+					return err
+				}
+				s.children = append(s.children, child)
+				continue
+			}
+			if !strings.HasSuffix(name, ".md") {
+				continue
+			}
+			if name == "index.md" {
+				s.hasIndex = true
+				if ext, ok := mdnavhref(childAbs); ok {
+					s.href = ext
+				}
+			} else {
+				href := "/" + filepath.ToSlash(strings.TrimSuffix(childRel, ".md")+".html")
+				if ext, ok := mdnavhref(childAbs); ok {
+					href = ext
+				}
+				s.pages = append(s.pages, page{
+					title: gentitle(name),
+					href:  href,
+				})
+			}
+		}
+		return nil
+	}
+
+	if err := walk(src, "", &root); err != nil {
+		return nil, err
+	}
+	return []sect{root}, nil
+}
+
+func findCurSect(sects []sect, cur string) *sect {
+	for i := range sects {
+		s := &sects[i]
+		if s.href == cur {
+			return s
+		}
+		for _, p := range s.pages {
+			if p.href == cur {
+				return s
+			}
+		}
+		if found := findCurSect(s.children, cur); found != nil {
+			return found
+		}
+	}
+	return nil
 }
 
-func copyfile(src, dst string) error {
-	in, err := os.Open(src)
-	if err != nil {
+func navX(roots []sect, cur string) string {
+	if len(roots) == 0 {
+		return ""
+	}
+	root := &roots[0]
+
+	var path []*sect
+	var findPath func(s *sect) bool
+	findPath = func(s *sect) bool {
+		path = append(path, s)
+		if s.href == cur {
+			return true
+		}
+		for _, p := range s.pages {
+			if p.href == cur {
+				return true
+			}
+		}
+		for i := range s.children {
+			if findPath(&s.children[i]) {
+				return true
+			}
+		}
+		path = path[:len(path)-1]
+		return false
+	}
+	if !findPath(root) {
+		return ""
+	}
+
+	sectLI := func(b *strings.Builder, s *sect, active bool) {
+		b.WriteString(" <li>")
+		if s.hasIndex {
+			cls := ""
+			if active {
+				cls = " current"
+			}
+			b.WriteString(`<a` + cls + ` href="` + s.href + `">` + s.title + `</a>`)
+		} else {
+			b.WriteString(s.title)
+		}
+		b.WriteString("</li>\n")
+	}
+
+	var b strings.Builder
+
+	{
+		var active1 *sect
+
+		if len(path) > 1 {
+			active1 = path[1]
+		}
+		b.WriteString("<ul>\n")
+		sectLI(&b, root, len(path) == 1)
+
+		for i := range root.children {
+			c := &root.children[i]
+			sectLI(&b, c, c == active1)
+		}
+		b.WriteString("</ul>\n")
+	}
+
+	for depth := 1; depth < len(path); depth++ {
+		s := path[depth]
+		if len(s.children) == 0 && len(s.pages) == 0 {
+			continue
+		}
+		var activeChild *sect
+		if depth+1 < len(path) {
+			activeChild = path[depth+1]
+		}
+		b.WriteString("<ul>\n")
+		for i := range s.children {
+			c := &s.children[i]
+			sectLI(&b, c, c == activeChild)
+		}
+		for _, p := range s.pages {
+			cls := ""
+			if p.href == cur {
+				cls = " current"
+			}
+			b.WriteString(` <li><a` + cls + ` href="` + p.href + `">` + p.title + `</a></li>` + "\n")
+		}
+		b.WriteString("</ul>\n")
+	}
+
+	return b.String()
+}
+
+func navY(roots []sect, cur string) string {
+	curSect := findCurSect(roots, cur)
+
+	var isAncestor func(s *sect) bool
+	isAncestor = func(s *sect) bool {
+		if s == curSect {
+			return true
+		}
+		for i := range s.children {
+			if isAncestor(&s.children[i]) {
+				return true
+			}
+		}
+		return false
+	}
+
+	var b strings.Builder
+	var renderSects func(sects []sect, depth int)
+	renderSects = func(sects []sect, depth int) {
+		indent := strings.Repeat(" ", depth)
+		b.WriteString(indent + "<ul>\n")
+		for i := range sects {
+			s := &sects[i]
+			b.WriteString(indent + " <li>")
+			if s.hasIndex {
+				cls := ""
+				if s == curSect {
+					cls = " current"
+				}
+				b.WriteString(`<a` + cls + ` href="` + s.href + `">` + s.title + `</a>`)
+			} else {
+				b.WriteString(s.title)
+			}
+
+			if isAncestor(s) {
+				if len(s.pages) > 0 || len(s.children) > 0 {
+					b.WriteString("\n")
+					if len(s.children) > 0 {
+						renderSects(s.children, depth+2)
+					}
+					if len(s.pages) > 0 {
+						b.WriteString(indent + "  <ul>\n")
+						for _, p := range s.pages {
+							cls := ""
+							if p.href == cur {
+								cls = " current"
+							}
+							b.WriteString(indent + `   <li><a` + cls + ` href="` + p.href + `">` + p.title + `</a></li>` + "\n")
+						}
+						b.WriteString(indent + "  </ul>\n")
+					}
+					b.WriteString(indent + " ")
+				}
+			}
+			b.WriteString("</li>\n")
+		}
+		b.WriteString(indent + "</ul>\n")
+	}
+	renderSects(roots, 0)
+	return b.String()
+}
+
+func render(tpl, pageTitle, nav, content string, cfg config) string {
+	compositeTitle := cfg.SiteTitle + " | " + pageTitle
+	r := strings.NewReplacer(
+		"{{TITLE}}", compositeTitle,
+		"{{NAV}}", nav,
+		"{{PAGE_TITLE}}", pageTitle,
+		"{{CONTENT}}", content,
+		"{{FOOTER_TEXT}}", cfg.FooterText,
+	)
+	return r.Replace(tpl)
+}
+
+func writepage(outpath, html string) error {
+	if err := os.MkdirAll(filepath.Dir(outpath), 0755); 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
+	return os.WriteFile(outpath, []byte(html), 0644)
 }
 
 func main() {
-	if len(os.Args) != 3 {
-		fmt.Fprintln(os.Stderr, "usage: qwb <in> <out>")
-		os.Exit(1)
+	if len(os.Args) < 3 {
+		fmt.Println("usage: qwb <src> <out> [-x|-y]")
+		return
 	}
 	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)
+	mode := "x"
+	if len(os.Args) == 4 {
+		switch os.Args[3] {
+		case "-x":
+			mode = "x"
+		case "-y":
+			mode = "y"
+		default:
+			fmt.Println("usage: qwb <src> <out> [-x|-y]")
+			return
 		}
 	}
 
-	cfg := loadconfig(src)
-	tmpl := loadtemplate(cfg)
-	rootsec, subs := collectsections(src, cfg.headertext)
+	cfg := loadcfg(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)
-		pageTitle, body := extracth1(body)
-		if pageTitle == "" {
-			pageTitle = cfg.headertext
-		}
-		cur := "/" + strings.TrimSuffix(rel, ".md") + ".html"
-		title := cfg.headertext
-		if filepath.Base(path) != "index.md" {
-			title = cfg.headertext + " | " + pageTitle
-		}
-		pg := strings.ReplaceAll(tmpl, "{{TITLE}}", title)
-		pg = strings.ReplaceAll(pg, "{{SITE_TITLE}}", pageTitle)
-		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)
-	})
-
+	sects, err := scansrc(src)
 	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
+		fmt.Fprintln(os.Stderr, "scan:", err)
 		os.Exit(1)
 	}
-}
\ No newline at end of file
+
+	var process func(dir, rel string)
+	process = func(dir, rel string) {
+		entries, err := os.ReadDir(dir)
+		if err != nil {
+			return
+		}
+		for _, e := range entries {
+			name := e.Name()
+			childRel := filepath.Join(rel, name)
+			childAbs := filepath.Join(dir, name)
+			if e.IsDir() {
+				process(childAbs, childRel)
+				continue
+			}
+			if !strings.HasSuffix(name, ".md") {
+
+				dst := filepath.Join(out, childRel)
+				_ = os.MkdirAll(filepath.Dir(dst), 0755)
+				if data, err := os.ReadFile(childAbs); err == nil {
+					_ = os.WriteFile(dst, data, 0644)
+				}
+				continue
+			}
+			htmlRel := strings.TrimSuffix(childRel, ".md") + ".html"
+			href := "/" + filepath.ToSlash(htmlRel)
+
+			var nav string
+			if mode == "y" {
+				nav = navY(sects, href)
+			} else {
+				nav = navX(sects, href)
+			}
+
+			pageTitle := mdtitle(childAbs)
+			if pageTitle == "" {
+				pageTitle = gentitle(name)
+			}
+
+			content, err := md2html(childAbs)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "md2html %s: %v\n", childAbs, err)
+				content = "<p>render error</p>"
+			}
+
+			html := render(tpldefault, pageTitle, nav, content, cfg)
+			outpath := filepath.Join(out, htmlRel)
+			if err := writepage(outpath, html); err != nil {
+				fmt.Fprintf(os.Stderr, "write %s: %v\n", outpath, err)
+			} else {
+				fmt.Println("→", outpath)
+			}
+		}
+	}
+
+	process(src, "")
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/index.md	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,1 @@
+Hello world
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/project/about.md	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,3 @@
+# About my Project
+
+Information about my project
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/project/docs/authors.md	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,2 @@
+# Authors
+Authors of the project
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/project/docs/index.md	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,3 @@
+# My Project Documentation
+
+y
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/project/index.md	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,3 @@
+# My Project
+
+My project
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/qwb.ini	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,4 @@
+[site]
+SiteTitle=My Site
+FooterText=&copy; Me
+style=/x.css
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/x.css	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,145 @@
+/* 1. The Reset: Neutralizing browser quirks across engines */
+* { 
+    box-sizing: border-box; 
+    margin: 0; 
+    padding: 0;
+}
+
+/* 2. Typography: Manrope with system fallbacks for lightweight browsers */
+@font-face {
+    font-family: 'Manrope';
+    src: url('https://qwaderton.org/Manrope-VariableFont_wght.woff2') format('woff2-variations');
+    font-weight: 100 800;
+}
+
+html {
+    font-size: 100%;
+    -webkit-text-size-adjust: 100%;
+}
+
+body {
+    font-family: 'Manrope', system-ui, -apple-system, sans-serif;
+    line-height: 1.6;
+    color: #333;
+    background: #fff;
+    max-width: 45rem; /* The "Golden Width" for readability */
+    margin: 2rem auto;
+    padding: 0 1rem;
+    text-rendering: optimizeLegibility;
+}
+
+/* 3. Semantic Layout: No classes needed */
+header, nav, main, section, footer {
+    display: block; /* Fix for older browsers */
+    margin-bottom: 2rem;
+}
+
+footer {
+    border-top: 1px solid #eee;
+    padding-top: 0.5rem;
+}
+
+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;
+}
+
+nav ul:last-child:not(:first-child) {
+    text-align: right;
+}
+
+nav li {
+    display: inline;
+    margin-right: 1rem;
+}
+
+nav a[current] {
+    font-weight: bold;
+}
+
+/* 4. Elements */
+h1, h2, h3, h4 {
+    line-height: 1.2;
+    margin-top: 2rem;
+    margin-bottom: 1rem;
+    font-weight: 700;
+}
+
+a { color: #0070f3; text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+p, blockquote, ul, ol, dl, table, pre {
+    margin-bottom: 1.5rem;
+}
+
+blockquote {
+    border-left: 4px solid #eee;
+    padding-left: 1rem;
+    font-style: italic;
+}
+
+img, video {
+    max-width: 100%;
+    height: auto;
+}
+
+/* 5. Tables & Forms: Standardized for basic engines */
+table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+th, td {
+    padding: 0.5rem;
+    text-align: left;
+    border-bottom: 1px solid #eee;
+}
+
+input, textarea, button, select {
+    font-family: inherit;
+    font-size: 1rem;
+    padding: 0.5rem;
+    margin-bottom: 1rem;
+    display: block;
+}
+
+input[type="checkbox"],input[type="radio"] {
+    display: inline
+}
+
+button, select {
+    width: auto;
+    cursor: pointer;
+    background: #333;
+    color: #fff;
+    border: none;
+    border-radius: 4px;
+}
+
+@media (max-width: 46rem) {
+    li {
+        margin-left: 1rem;
+    }
+}
+
+/* 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, ul, footer { border-color: #333 !important; }
+    button { background: #eee; color: #111; }
+    input, textarea, button { background-color: #222; color: #ddd; }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/z.css	Tue Mar 17 22:18:02 2026 +0500
@@ -0,0 +1,8 @@
+nav ul {
+    padding-bottom: 0.5rem;
+    display: inline-block;
+}
+
+nav ul:last-child {
+    float: right;
+}