From 2e124cde02101bf795e57e9ef5e269bcbabed39a Mon Sep 17 00:00:00 2001 From: yhirose Date: Mon, 2 Mar 2026 00:48:14 -0500 Subject: [PATCH] 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 --- docs-gen/README.md | 291 ++++++++++++++++---------- docs-gen/defaults/config.toml | 10 +- docs-gen/defaults/pages/en/index.md | 2 +- docs-gen/defaults/pages/ja/index.md | 2 +- docs-gen/defaults/static/css/main.css | 9 +- docs-gen/src/builder.rs | 6 +- docs-gen/src/config.rs | 36 +++- docs-gen/src/markdown.rs | 16 +- docs-src/config.toml | 6 +- docs/css/main.css | 9 +- 10 files changed, 225 insertions(+), 162 deletions(-) diff --git a/docs-gen/README.md b/docs-gen/README.md index 4923a48..4ad289c 100644 --- a/docs-gen/README.md +++ b/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 + +# 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 ``` -## Usage +--- + +## Commands + +### `init [DIR]` + +Creates a new project scaffold in `DIR` (default: `.`). + +Generated files: ``` -docs-gen [SRC] [--out OUT] +config.toml +pages/ + en/index.md + ja/index.md +templates/ + base.html + page.html + portal.html +static/ + css/main.css + js/main.js ``` -- `SRC` — Source directory containing `config.toml` (default: `.`) -- `--out OUT` — Output directory (default: `docs`) +Existing files are never overwritten. -Example: +--- -``` -./docs-gen/target/release/docs-gen docs-src --out docs -``` +### `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 = '...' # Optional inline SVG icon + +[[nav]] +label = "GitHub" +url = "https://github.com/your/repo" # External URL +icon_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]` -`base_path` controls the URL prefix prepended to all generated links, CSS/JS paths, and redirects. +| 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. | -| Value | Use case | -|---|---| -| `"/my-project"` | GitHub Pages (`https://user.github.io/my-project/`) | -| `""` | Local development at `http://localhost:8000/` | +### `[[nav]]` — Toolbar Buttons -Leave empty for local-only use; set to `"/"` before deploying to GitHub Pages. +Defines buttons in the site header. Each entry supports: -### `highlight` +| Key | Required | Description | +|-----|----------|-------------| +| `label` | yes | Button label text | +| `path` | no | Internal section path relative to `/` (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 | -When `theme_light` is set, code blocks are rendered twice (dark and light) and toggled via CSS classes `.code-dark` / `.code-light`. +### `[i18n]` + +| Key | Required | Description | +|-----|----------|-------------| +| `langs` | yes | List of language codes. At least one is required. The first entry is used as the default language. | + +### `[highlight]` + +| 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 +--- -Every `.md` file requires YAML frontmatter: +## Writing Pages + +### 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` | `/en/` | `en/index.html` | -| `en/tour/index.md` | `/en/tour/` | `en/tour/index.html` | -| `en/tour/01-foo.md` | `/en/tour/01-foo/` | `en/tour/01-foo/index.html` | +| File | URL | +|------|-----| +| `en/index.md` | `/en/` | +| `en/guide/index.md` | `/en/guide/` | +| `en/guide/01-intro.md` | `/en/guide/01-intro/` | -A root `index.html` is generated automatically, redirecting `/` to `//` (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: - -```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/ -``` - -For a plain local preview (no prefix), set `base_path = ""` and open `http://localhost:8000/`. - -## Navigation - -Navigation is generated automatically from the directory structure: +Sidebar navigation is generated automatically: - 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) +- `index.md` at the language root uses `portal.html` (no sidebar) +- All other pages use `page.html` (with sidebar) + +--- + +## Customizing Templates and Assets + +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. + +Three templates are available: + +| Template | Used for | +|----------|----------| +| `base.html` | Shared layout: ``, 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 | +| `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 +### `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 | +| 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 | diff --git a/docs-gen/defaults/config.toml b/docs-gen/defaults/config.toml index 47a780d..19b544e 100644 --- a/docs-gen/defaults/config.toml +++ b/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 = '' [i18n] -default_lang = "en" -langs = ["en", "ja"] +langs = ["en"] [highlight] theme = "base16-ocean.dark" -theme_light = "InspiredGitHub" diff --git a/docs-gen/defaults/pages/en/index.md b/docs-gen/defaults/pages/en/index.md index 2ccfc54..858230b 100644 --- a/docs-gen/defaults/pages/en/index.md +++ b/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. diff --git a/docs-gen/defaults/pages/ja/index.md b/docs-gen/defaults/pages/ja/index.md index 96312d7..3c792c0 100644 --- a/docs-gen/defaults/pages/ja/index.md +++ b/docs-gen/defaults/pages/ja/index.md @@ -3,7 +3,7 @@ title: ようこそ order: 0 --- -# ようこそ +## ようこそ ドキュメントサイトのトップページです。 diff --git a/docs-gen/defaults/static/css/main.css b/docs-gen/defaults/static/css/main.css index cd99f44..60e3c3e 100644 --- a/docs-gen/defaults/static/css/main.css +++ b/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; diff --git a/docs-gen/src/builder.rs b/docs-gen/src/builder.rs index 18e5cef..7d6621b 100644 --- a/docs-gen/src/builder.rs +++ b/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<()> {

Redirecting to {base_path}/{default_lang}/...

"#, - default_lang = config.i18n.default_lang, + default_lang = config.i18n.default_lang(), base_path = base_path, ); diff --git a/docs-gen/src/config.rs b/docs-gen/src/config.rs index bbc4e0a..60f767b 100644 --- a/docs-gen/src/config.rs +++ b/docs-gen/src/config.rs @@ -27,21 +27,39 @@ pub struct NavLink { pub struct Site { pub title: String, pub version: Option, - 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, #[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, } +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, - pub theme_light: Option, } 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()) - } } diff --git a/docs-gen/src/markdown.rs b/docs-gen/src/markdown.rs index efbb120..6e8da4a 100644 --- a/docs-gen/src/markdown.rs +++ b/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, } 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!( - "
{}
{}
", - dark_html, light_html - ) - } else { - dark_html - } + self.highlight_with_theme(code, syntax, &self.theme_name) } fn highlight_with_theme( diff --git a/docs-src/config.toml b/docs-src/config.toml index bffa7c2..dd83e68 100644 --- a/docs-src/config.toml +++ b/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 = '' [i18n] -default_lang = "en" langs = ["en", "ja"] [highlight] theme = "base16-eighties.dark" -theme_light = "base16-ocean.light" diff --git a/docs/css/main.css b/docs/css/main.css index cd99f44..60e3c3e 100644 --- a/docs/css/main.css +++ b/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;