Mercurial Hosting > qwb
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
--- /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
--- /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
--- /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
--- /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
--- /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
--- /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; } +} +
--- /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>© 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
