Mercurial Hosting > qwb
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=© 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>© 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>© 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>© 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>© 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>© 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>© 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=© 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: "© 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 := §s[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 := §s[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=© 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; } +}
