changeset 0:ac64aa92dea1

initial
author Atarwn Gard <a@qwa.su>
date Fri, 13 Mar 2026 13:13:07 +0500
parents
children 72124c0555c8
files README.md go.mod in/index.md in/project/about.md in/project/index.md in/x.css qwb.go
diffstat 7 files changed, 439 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
diff -r 000000000000 -r ac64aa92dea1 README.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,3 @@
+# Qwaderton's Website Builder
+
+qwb is a very stripped fork of kew, optimized specificly for x.css
\ No newline at end of file
diff -r 000000000000 -r ac64aa92dea1 go.mod
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/go.mod	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,3 @@
+module qwb
+
+go 1.24
diff -r 000000000000 -r ac64aa92dea1 in/index.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/in/index.md	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,1 @@
+Hello world
\ No newline at end of file
diff -r 000000000000 -r ac64aa92dea1 in/project/about.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/in/project/about.md	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,1 @@
+Information about my project
\ No newline at end of file
diff -r 000000000000 -r ac64aa92dea1 in/project/index.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/in/project/index.md	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,1 @@
+My project
\ No newline at end of file
diff -r 000000000000 -r ac64aa92dea1 in/x.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/in/x.css	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,149 @@
+/* 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 {
+    float: left;
+    width: 50%;
+}
+
+footer p:nth-child(2) {
+    text-align: right;
+}
+
+nav ul {
+    list-style: none;
+    padding-bottom: 0.5rem;
+}
+
+nav ul:first-child:not(:last-child) {
+    float: left;
+}
+
+nav ul:last-child {
+    border-bottom: 1px solid #eee;
+}
+
+nav ul:nth-child(2) {
+    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 for modern browsers (ignored by NetSurf/Chawan) */
+@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; }
+    button { background: #eee; color: #111; }
+    input, textarea, button { background-color: #222; color: #ddd; }
+    nav ul:last-child, footer { border-color: #333; }
+}
+
diff -r 000000000000 -r ac64aa92dea1 qwb.go
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qwb.go	Fri Mar 13 13:13:07 2026 +0500
@@ -0,0 +1,281 @@
+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>&copy; 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)
+	}
+}
\ No newline at end of file