1
0
Эх сурвалжийг харах

Enhance documentation and configuration for static site generator

- Update README for clarity and quick start instructions
- Refine default config.toml with hostname and base path
- Adjust index.md files for consistent heading levels
- Simplify CSS for code block styling and remove unnecessary theme switching
- Refactor SiteConfig to derive full base URL from hostname and base path
- Update MarkdownRenderer to remove light theme handling
yhirose 1 сар өмнө
parent
commit
2e124cde02

+ 185 - 108
docs-gen/README.md

@@ -1,137 +1,218 @@
 # docs-gen
 
-A simple static site generator written in Rust. Designed for multi-language documentation sites with Markdown content, Tera templates, and syntax highlighting.
+A static site generator for multi-language documentation. Markdown content, Tera templates, and syntax highlighting — all in a single binary.
 
-## Build
+## Quick Start
 
-```
-cargo build --release --manifest-path docs-gen/Cargo.toml
-```
+```bash
+# 1. Scaffold a new project
+docs-gen init my-docs
 
-## Usage
+# 2. Start the local dev server with live-reload
+docs-gen serve my-docs --open
 
+# 3. Build for production
+docs-gen build my-docs --out docs
 ```
-docs-gen [SRC] [--out OUT]
-```
 
-- `SRC` — Source directory containing `config.toml` (default: `.`)
-- `--out OUT` — Output directory (default: `docs`)
+---
+
+## Commands
 
-Example:
+### `init [DIR]`
+
+Creates a new project scaffold in `DIR` (default: `.`).
+
+Generated files:
 
 ```
-./docs-gen/target/release/docs-gen docs-src --out docs
+config.toml
+pages/
+  en/index.md
+  ja/index.md
+templates/
+  base.html
+  page.html
+  portal.html
+static/
+  css/main.css
+  js/main.js
 ```
 
+Existing files are never overwritten.
+
+---
+
+### `serve [SRC] [--port PORT] [--open]`
+
+Builds the site into a temporary directory and serves it locally. Watches for changes and live-reloads the browser automatically.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `SRC` | `.` | Source directory |
+| `--port` | `8080` | HTTP server port |
+| `--open` | — | Open browser on startup |
+
+---
+
+### `build [SRC] [--out OUT]`
+
+Generates the static site from source.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `SRC` | `.` | Source directory |
+| `--out` | `docs` | Output directory |
+
+---
+
 ## Source Directory Structure
 
+Only `config.toml` and `pages/` are required. `templates/` and `static/` are optional — when absent, built-in defaults are used automatically.
+
 ```
-docs-src/
-├── config.toml          # Site configuration
-├── pages/               # Markdown content (one subdirectory per language)
+my-docs/
+├── config.toml          # Site configuration (required)
+├── pages/               # Markdown content (required)
 │   ├── en/
-│   │   ├── index.md     # Portal page (no sidebar)
-│   │   ├── tour/
-│   │   │   ├── index.md # Section index
-│   │   │   ├── 01-getting-started.md
-│   │   │   └── ...
-│   │   └── cookbook/
-│   │       └── index.md
+│   │   ├── index.md         # Portal page (homepage, no sidebar)
+│   │   └── guide/
+│   │       ├── index.md     # Section index
+│   │       ├── 01-intro.md
+│   │       └── 02-usage.md
 │   └── ja/
-│       └── ...          # Same structure as en/
-├── templates/           # Tera HTML templates
-│   ├── base.html        # Base layout (header, scripts)
-│   ├── page.html        # Content page with sidebar navigation
-│   └── portal.html      # Portal page without sidebar
-└── static/              # Static assets (copied as-is to output root)
-    ├── css/
-    └── js/
+│       └── ...
+├── templates/           # Override built-in HTML templates (optional)
+│   ├── base.html
+│   ├── page.html
+│   └── portal.html
+└── static/              # Override built-in CSS/JS/assets (optional)
+    ├── css/main.css
+    └── js/main.js
 ```
 
+---
+
 ## config.toml
 
 ```toml
 [site]
 title = "My Project"
-base_url = "https://example.github.io/my-project"
-base_path = "/my-project"
+version = "1.0.0"                           # Optional. Shown in header.
+hostname = "https://example.github.io"      # Optional. Combined with base_path for full URLs.
+base_path = "/my-project"                   # URL prefix. Use "" for local-only.
+
+[[nav]]
+label = "Guide"
+path = "guide/"                             # Internal section path (resolved per language)
+icon_svg = '<svg ...>...</svg>'             # Optional inline SVG icon
+
+[[nav]]
+label = "GitHub"
+url = "https://github.com/your/repo"        # External URL
+icon_svg = '<svg ...>...</svg>'
 
 [i18n]
-default_lang = "en"
-langs = ["en", "ja"]
+langs = ["en", "ja"]    # First entry is the default language
 
 [highlight]
-theme = "base16-eighties.dark"        # Dark mode syntax theme (syntect built-in)
-theme_light = "base16-ocean.light"    # Light mode syntax theme (optional)
+theme = "base16-eighties.dark"   # Syntax highlighting theme
 ```
 
-### `base_path`
+### `[site]`
+
+| Key | Required | Description |
+|-----|----------|-------------|
+| `title` | yes | Site title displayed in the header |
+| `version` | no | Version string displayed in the header |
+| `hostname` | no | Base hostname (e.g. `"https://user.github.io"`). Combined with `base_path` to form `site.base_url` in templates. |
+| `base_path` | no | URL path prefix. Use `"/repo-name"` for GitHub Pages, `""` for local development. |
+
+### `[[nav]]` — Toolbar Buttons
+
+Defines buttons in the site header. Each entry supports:
 
-`base_path` controls the URL prefix prepended to all generated links, CSS/JS paths, and redirects.
+| Key | Required | Description |
+|-----|----------|-------------|
+| `label` | yes | Button label text |
+| `path` | no | Internal section path relative to `<lang>/` (e.g. `"guide/"`). Resolved using the current language. |
+| `url` | no | Absolute external URL. Takes precedence over `path` if both are set. |
+| `icon_svg` | no | Inline SVG markup displayed as an icon |
 
-| Value | Use case |
-|---|---|
-| `"/my-project"` | GitHub Pages (`https://user.github.io/my-project/`) |
-| `""` | Local development at `http://localhost:8000/` |
+### `[i18n]`
 
-Leave empty for local-only use; set to `"/<repo-name>"` before deploying to GitHub Pages.
+| Key | Required | Description |
+|-----|----------|-------------|
+| `langs` | yes | List of language codes. At least one is required. The first entry is used as the default language. |
 
-### `highlight`
+### `[highlight]`
 
-When `theme_light` is set, code blocks are rendered twice (dark and light) and toggled via CSS classes `.code-dark` / `.code-light`.
+| Key | Default | Description |
+|-----|---------|-------------|
+| `theme` | `base16-ocean.dark` | Syntax highlighting theme name (syntect built-in) |
 
 Available themes: `base16-ocean.dark`, `base16-ocean.light`, `base16-eighties.dark`, `base16-mocha.dark`, `InspiredGitHub`, `Solarized (dark)`, `Solarized (light)`.
 
-## Markdown Frontmatter
+---
+
+## Writing Pages
+
+### Frontmatter
 
-Every `.md` file requires YAML frontmatter:
+Every `.md` file must begin with YAML frontmatter:
 
 ```yaml
 ---
-title: "Page Title"
+title: "Getting Started"
 order: 1
 ---
+
+Page content goes here...
 ```
 
-| Field    | Required | Description |
-|----------|----------|-------------|
-| `title`  | yes      | Page title shown in heading and browser tab |
-| `order`  | no       | Sort order within the section (default: `0`) |
-| `status` | no       | Set to `"draft"` to show a DRAFT banner |
+| Field | Required | Description |
+|-------|----------|-------------|
+| `title` | yes | Page title shown in the heading and browser tab |
+| `order` | no | Sort order within the section (default: `0`) |
+| `status` | no | Set to `"draft"` to display a DRAFT banner |
 
-## URL Routing
+### URL Routing
 
-Markdown files are mapped to URLs as follows:
+Files are mapped to URLs as follows:
 
-| File path             | URL                         | Output file                         |
-|-----------------------|-----------------------------|-------------------------------------|
-| `en/index.md`         | `<base_path>/en/`           | `en/index.html`                     |
-| `en/tour/index.md`    | `<base_path>/en/tour/`      | `en/tour/index.html`                |
-| `en/tour/01-foo.md`   | `<base_path>/en/tour/01-foo/` | `en/tour/01-foo/index.html`       |
+| File | URL |
+|------|-----|
+| `en/index.md` | `<base_path>/en/` |
+| `en/guide/index.md` | `<base_path>/en/guide/` |
+| `en/guide/01-intro.md` | `<base_path>/en/guide/01-intro/` |
 
-A root `index.html` is generated automatically, redirecting `<base_path>/` to `<base_path>/<default_lang>/` (respecting `localStorage` preference).
+The root `index.html` is generated automatically and redirects to the default language, respecting the user's `localStorage` language preference.
 
-## Local Debugging vs GitHub Pages
+### Sidebar Navigation
 
-To preview locally with the same URL structure as GitHub Pages, set `base_path = "/cpp-httplib"` in `config.toml`, then:
+Sidebar navigation is generated automatically:
 
-```bash
-./docs-gen/target/release/docs-gen docs-src --out /tmp/test/cpp-httplib
-cd /tmp/test && python3 -m http.server
-# Open http://localhost:8000/cpp-httplib/
-```
+- Each subdirectory under a language becomes a **section**
+- The section's `index.md` title is used as the section heading
+- Pages within a section are sorted by `order`, then by filename
+- `index.md` at the language root uses `portal.html` (no sidebar)
+- All other pages use `page.html` (with sidebar)
 
-For a plain local preview (no prefix), set `base_path = ""` and open `http://localhost:8000/`.
+---
 
-## Navigation
+## Customizing Templates and Assets
 
-Navigation is generated automatically from the directory structure:
+When `templates/` or `static/` directories exist in the source, files there override the built-in defaults. Use `docs-gen init` to generate the defaults as a starting point.
 
-- Each subdirectory under a language becomes a **section**
-- The section's `index.md` title is used as the section heading
-- Pages within a section are sorted by `order`, then by filename
-- `portal.html` template is used for root `index.md` (no sidebar)
-- `page.html` template is used for all other pages (with sidebar)
+Three templates are available:
+
+| Template | Used for |
+|----------|----------|
+| `base.html` | Shared layout: `<head>`, header, footer, scripts |
+| `page.html` | Content pages with sidebar |
+| `portal.html` | Homepage (`index.md` at language root), no sidebar |
+
+---
 
 ## Template Variables
 
@@ -139,37 +220,33 @@ Templates use [Tera](https://keats.github.io/tera/) syntax. Available variables:
 
 ### All templates
 
-| Variable      | Type   | Description |
-|---------------|--------|-------------|
-| `page.title`  | string | Page title from frontmatter |
-| `page.url`    | string | Page URL path |
+| Variable | Type | Description |
+|----------|------|-------------|
+| `page.title` | string | Page title from frontmatter |
+| `page.url` | string | Page URL path |
 | `page.status` | string? | `"draft"` or null |
-| `content`     | string | Rendered HTML content (use `{{ content \| safe }}`) |
-| `lang`        | string | Current language code |
-| `site.title`  | string | Site title from config |
-| `site.base_url` | string | Base URL from config |
-| `site.base_path` | string | Base path prefix (e.g. `"/cpp-httplib"` or `""`) |
-| `site.langs`  | list   | Available language codes |
-
-### page.html only
-
-| Variable           | Type   | Description |
-|--------------------|--------|-------------|
-| `nav`              | list   | Navigation sections |
-| `nav[].title`      | string | Section title |
-| `nav[].url`        | string | Section URL |
-| `nav[].active`     | bool   | Whether this section contains the current page |
-| `nav[].children`   | list   | Child pages |
+| `content` | string | Rendered HTML (use `{{ content \| safe }}`) |
+| `lang` | string | Current language code |
+| `site.title` | string | Site title |
+| `site.version` | string? | Site version |
+| `site.base_url` | string | Full base URL (`hostname` + `base_path`) |
+| `site.base_path` | string | URL path prefix |
+| `site.langs` | list | All language codes |
+| `site.nav` | list | Toolbar button entries |
+| `site.nav[].label` | string | Button label |
+| `site.nav[].url` | string? | External URL (if set) |
+| `site.nav[].path` | string? | Internal section path (if set) |
+| `site.nav[].icon_svg` | string? | Inline SVG icon (if set) |
+
+### `page.html` only
+
+| Variable | Type | Description |
+|----------|------|-------------|
+| `nav` | list | Sidebar sections |
+| `nav[].title` | string | Section title |
+| `nav[].url` | string | Section index URL |
+| `nav[].active` | bool | True if this section contains the current page |
+| `nav[].children` | list | Pages within this section |
 | `nav[].children[].title` | string | Page title |
-| `nav[].children[].url`   | string | Page URL |
-| `nav[].children[].active` | bool  | Whether this is the current page |
-
-## Dependencies
-
-- [pulldown-cmark](https://crates.io/crates/pulldown-cmark) — Markdown parsing
-- [tera](https://crates.io/crates/tera) — Template engine
-- [syntect](https://crates.io/crates/syntect) — Syntax highlighting
-- [walkdir](https://crates.io/crates/walkdir) — Directory traversal
-- [serde](https://crates.io/crates/serde) / [serde_yml](https://crates.io/crates/serde_yml) / [toml](https://crates.io/crates/toml) — Serialization
-- [clap](https://crates.io/crates/clap) — CLI argument parsing
-- [anyhow](https://crates.io/crates/anyhow) — Error handling
+| `nav[].children[].url` | string | Page URL |
+| `nav[].children[].active` | bool | True if this is the current page |

+ 4 - 6
docs-gen/defaults/config.toml

@@ -1,7 +1,7 @@
 [site]
 title = "My Docs"
-base_url = "https://example.com"
-base_path = ""
+hostname = "https://example.github.io"
+base_path = "/my-project"
 
 # [[nav]]
 # label = "Guide"
@@ -10,12 +10,10 @@ base_path = ""
 # [[nav]]
 # label = "GitHub"
 # url = "https://github.com/your/repo"
-# icon = "github"
+# icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>'
 
 [i18n]
-default_lang = "en"
-langs = ["en", "ja"]
+langs = ["en"]
 
 [highlight]
 theme = "base16-ocean.dark"
-theme_light = "InspiredGitHub"

+ 1 - 1
docs-gen/defaults/pages/en/index.md

@@ -3,7 +3,7 @@ title: Welcome
 order: 0
 ---
 
-# Welcome
+## Welcome
 
 This is the home page of your documentation site.
 

+ 1 - 1
docs-gen/defaults/pages/ja/index.md

@@ -3,7 +3,7 @@ title: ようこそ
 order: 0
 ---
 
-# ようこそ
+## ようこそ
 
 ドキュメントサイトのトップページです。
 

+ 1 - 8
docs-gen/defaults/static/css/main.css

@@ -304,7 +304,7 @@ a:hover {
 }
 
 .content article pre {
-  background: var(--bg-code) !important;
+  background: var(--bg-code);
   color: var(--text-code);
   padding: 16px;
   border-radius: 4px;
@@ -426,13 +426,6 @@ a:hover {
   --nav-section-active: #333;
 }
 
-/* Code block theme switching */
-.code-light { display: none; }
-.code-dark { display: block; }
-
-[data-theme="light"] .code-light { display: block; }
-[data-theme="light"] .code-dark { display: none; }
-
 /* Theme toggle */
 .theme-toggle {
   display: flex;

+ 3 - 3
docs-gen/src/builder.rs

@@ -55,7 +55,7 @@ struct Page {
 
 pub fn build(src: &Path, out: &Path) -> Result<()> {
     let config = SiteConfig::load(src)?;
-    let renderer = MarkdownRenderer::new(config.highlight_theme(), config.highlight_theme_light());
+    let renderer = MarkdownRenderer::new(config.highlight_theme());
 
     // Build Tera: start with embedded defaults, then override with user templates
     let tera = build_tera(src)?;
@@ -125,7 +125,7 @@ pub fn build(src: &Path, out: &Path) -> Result<()> {
             ctx.insert("site", &SiteContext {
                 title: config.site.title.clone(),
                 version: config.site.version.clone(),
-                base_url: config.site.base_url.clone(),
+                base_url: config.site.base_url(),
                 base_path: config.site.base_path.clone(),
                 langs: config.i18n.langs.clone(),
                 nav: config.nav.clone(),
@@ -351,7 +351,7 @@ fn generate_root_redirect(out: &Path, config: &SiteConfig) -> Result<()> {
 <p>Redirecting to <a href="{base_path}/{default_lang}/">{base_path}/{default_lang}/</a>...</p>
 </body>
 </html>"#,
-        default_lang = config.i18n.default_lang,
+        default_lang = config.i18n.default_lang(),
         base_path = base_path,
     );
 

+ 27 - 9
docs-gen/src/config.rs

@@ -27,21 +27,39 @@ pub struct NavLink {
 pub struct Site {
     pub title: String,
     pub version: Option<String>,
-    pub base_url: String,
+    /// Optional hostname (e.g. "https://example.github.io"). Combined with
+    /// base_path to form the full base URL.
+    pub hostname: Option<String>,
     #[serde(default)]
     pub base_path: String,
 }
 
+impl Site {
+    /// Returns the full base URL derived from hostname + base_path.
+    /// Falls back to base_path alone if hostname is not set.
+    pub fn base_url(&self) -> String {
+        match &self.hostname {
+            Some(h) => format!("{}{}", h.trim_end_matches('/'), self.base_path),
+            None => self.base_path.clone(),
+        }
+    }
+}
+
 #[derive(Debug, Deserialize)]
 pub struct I18n {
-    pub default_lang: String,
     pub langs: Vec<String>,
 }
 
+impl I18n {
+    /// Returns the default language, which is the first entry in langs.
+    pub fn default_lang(&self) -> &str {
+        self.langs.first().map(|s| s.as_str()).unwrap_or("en")
+    }
+}
+
 #[derive(Debug, Deserialize)]
 pub struct Highlight {
     pub theme: Option<String>,
-    pub theme_light: Option<String>,
 }
 
 impl SiteConfig {
@@ -51,6 +69,12 @@ impl SiteConfig {
             std::fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
         let mut config: SiteConfig =
             toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
+
+        // Validate required fields
+        if config.i18n.langs.is_empty() {
+            anyhow::bail!("[i18n] langs must not be empty. Please specify at least one language.");
+        }
+
         // Normalize base_path: strip trailing slash (but keep empty for root)
         let bp = config.site.base_path.trim_end_matches('/').to_string();
         config.site.base_path = bp;
@@ -63,10 +87,4 @@ impl SiteConfig {
             .and_then(|h| h.theme.as_deref())
             .unwrap_or("base16-ocean.dark")
     }
-
-    pub fn highlight_theme_light(&self) -> Option<&str> {
-        self.highlight
-            .as_ref()
-            .and_then(|h| h.theme_light.as_deref())
-    }
 }

+ 2 - 14
docs-gen/src/markdown.rs

@@ -17,16 +17,14 @@ pub struct MarkdownRenderer {
     syntax_set: SyntaxSet,
     theme_set: ThemeSet,
     theme_name: String,
-    theme_light_name: Option<String>,
 }
 
 impl MarkdownRenderer {
-    pub fn new(theme_name: &str, theme_light_name: Option<&str>) -> Self {
+    pub fn new(theme_name: &str) -> Self {
         Self {
             syntax_set: SyntaxSet::load_defaults_newlines(),
             theme_set: ThemeSet::load_defaults(),
             theme_name: theme_name.to_string(),
-            theme_light_name: theme_light_name.map(|s| s.to_string()),
         }
     }
 
@@ -95,17 +93,7 @@ impl MarkdownRenderer {
             .find_syntax_by_token(lang)
             .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
 
-        let dark_html = self.highlight_with_theme(code, syntax, &self.theme_name);
-
-        if let Some(ref light_name) = self.theme_light_name {
-            let light_html = self.highlight_with_theme(code, syntax, light_name);
-            format!(
-                "<div class=\"code-dark\">{}</div><div class=\"code-light\">{}</div>",
-                dark_html, light_html
-            )
-        } else {
-            dark_html
-        }
+        self.highlight_with_theme(code, syntax, &self.theme_name)
     }
 
     fn highlight_with_theme(

+ 1 - 5
docs-src/config.toml

@@ -1,9 +1,7 @@
 [site]
 title = "cpp-httplib"
 version = "0.35.0"
-base_url = "https://yhirose.github.io/cpp-httplib"
-# Base path for URL generation. Use "/cpp-httplib" for GitHub Pages,
-# or "" for local development (python3 -m http.server).
+hostname = "https://yhirose.github.io"
 base_path = "/cpp-httplib"
 
 [[nav]]
@@ -17,9 +15,7 @@ url = "https://github.com/yhirose/cpp-httplib"
 icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>'
 
 [i18n]
-default_lang = "en"
 langs = ["en", "ja"]
 
 [highlight]
 theme = "base16-eighties.dark"
-theme_light = "base16-ocean.light"

+ 1 - 8
docs/css/main.css

@@ -304,7 +304,7 @@ a:hover {
 }
 
 .content article pre {
-  background: var(--bg-code) !important;
+  background: var(--bg-code);
   color: var(--text-code);
   padding: 16px;
   border-radius: 4px;
@@ -426,13 +426,6 @@ a:hover {
   --nav-section-active: #333;
 }
 
-/* Code block theme switching */
-.code-light { display: none; }
-.code-dark { display: block; }
-
-[data-theme="light"] .code-light { display: block; }
-[data-theme="light"] .code-dark { display: none; }
-
 /* Theme toggle */
 .theme-toggle {
   display: flex;