Skip to content

Commit ee9ceb5

Browse files
authored
fix: move build info footer (#25)
* fix: move build info footer * justify footer left * rename files * fix wrapper * move timezone to dates * plan for config validation * fix title deduplication * rename files
1 parent 7b4b422 commit ee9ceb5

16 files changed

Lines changed: 198 additions & 18 deletions

File tree

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ A Python static site generator that works with Obsidian vaults and plain markdow
88

99
- [[Architecture]] - Build pipeline and module structure
1010
- [[Deployment]] - Hosting and CI/CD
11-
- [[markdown-support|Markdown Support]] - Supported syntax reference
11+
- [[Markdown Support]] - Supported syntax reference
1212
- [[Vision]] - Goals and design philosophy

docs/rockgarden.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ title = "Rockgarden"
33
clean_urls = true
44
output = "../_site"
55
ignore_patterns = []
6+
7+
[dates]
68
timezone = "US/Eastern"
79

810
[nav]
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Feature N11: Config Validation
2+
3+
## Status: Not Started (Phase B)
4+
5+
## Goal
6+
7+
Provide a `validate` CLI command that checks a `rockgarden.toml` for problems: unknown keys (likely typos), invalid values, missing required values, and theme-specific config issues. Themes should be able to declare what config keys they use so that unexpected or missing theme config can also be reported.
8+
9+
---
10+
11+
## Design
12+
13+
### Validation Categories
14+
15+
**Errors** (exit code 1):
16+
- TOML syntax errors
17+
- Invalid timezone (ZoneInfo lookup fails)
18+
- Theme declared as required config missing from TOML
19+
20+
**Warnings** (exit code 0, printed to stderr):
21+
- Unknown key in any config section (probable typo)
22+
- Source directory does not exist
23+
- `theme.name` set but `_themes/<name>/` directory not found
24+
- Theme manifest declares a required key but it's absent from `[theme]`
25+
26+
### Known-Key Validation
27+
28+
The existing dataclasses define all valid keys per section. Validation extracts field names via `dataclasses.fields()` and compares against what the TOML dict actually contains. Unknown keys get a warning.
29+
30+
Known sections: `site`, `build`, `theme`, `nav`, `toc`, `search`, `dates`. Unknown top-level sections also warn.
31+
32+
### Theme Manifest (`theme.toml`)
33+
34+
A theme can optionally ship a `theme.toml` in its directory (`_themes/<name>/theme.toml`) to declare metadata and any additional config keys it reads from the `[theme]` section.
35+
36+
```toml
37+
[theme]
38+
name = "pyohio"
39+
description = "PyOhio conference theme"
40+
version = "1.0.0"
41+
42+
[[theme.config]]
43+
key = "show_sponsors"
44+
type = "bool"
45+
required = true
46+
description = "Whether to show sponsor logos in the footer"
47+
48+
[[theme.config]]
49+
key = "primary_color"
50+
type = "string"
51+
required = false
52+
default = "#3490dc"
53+
description = "Primary brand color (CSS value)"
54+
```
55+
56+
When a theme manifest exists:
57+
- Its declared keys are added to the known-key set for `[theme]`, so they don't produce "unknown key" warnings
58+
- Any key marked `required = true` that is absent from `[theme]` produces a warning
59+
- Themes without a manifest are still valid — manifest is optional
60+
61+
---
62+
63+
## Implementation Plan
64+
65+
### 1. New module: `src/rockgarden/validation.py`
66+
67+
```python
68+
@dataclass
69+
class ValidationIssue:
70+
level: Literal["error", "warning"]
71+
message: str
72+
73+
def validate_config(config_dict: dict, source_dir: Path | None = None) -> list[ValidationIssue]:
74+
"""Validate a parsed TOML dict against known config schema."""
75+
...
76+
77+
def load_theme_manifest(theme_dir: Path) -> dict:
78+
"""Load and parse theme.toml from a theme directory. Returns {} if absent."""
79+
...
80+
```
81+
82+
Key checks in `validate_config()`:
83+
- Unknown top-level sections
84+
- Unknown keys per section (via `dataclasses.fields()` on each config class)
85+
- Timezone: `ZoneInfo(dates_data.get("timezone", "UTC"))` in try/except
86+
- Source dir existence (if provided / resolvable)
87+
- Theme dir existence + manifest loading if `theme.name` is set
88+
- Theme manifest required-key check
89+
90+
### 2. New CLI command in `src/rockgarden/cli.py`
91+
92+
```python
93+
@app.command()
94+
def validate(
95+
source: Annotated[Path | None, typer.Option("--source", "-s")] = None,
96+
config_file: Annotated[Path | None, typer.Option("--config", "-c")] = None,
97+
) -> None:
98+
"""Validate rockgarden configuration."""
99+
```
100+
101+
Same config/source resolution logic as `build` (auto-discovers `rockgarden.toml` in source dir). Reports issues to stderr, exits 1 on any errors.
102+
103+
Output format:
104+
```
105+
✓ No issues found.
106+
107+
# or:
108+
109+
Warning: [site] unknown key "titl" (did you mean "title"?)
110+
Warning: source directory "content" does not exist
111+
Error: [dates] invalid timezone "US/Easten" — ZoneInfoNotFoundError
112+
```
113+
114+
### 3. Known-key sets
115+
116+
Rather than hard-coding lists, derive from dataclasses at runtime:
117+
118+
```python
119+
import dataclasses
120+
from rockgarden.config import SiteConfig, BuildConfig, ThemeConfig, ...
121+
122+
KNOWN_KEYS = {
123+
"site": {f.name for f in dataclasses.fields(SiteConfig)},
124+
"build": {f.name for f in dataclasses.fields(BuildConfig)},
125+
...
126+
}
127+
```
128+
129+
Built-in `ThemeConfig` fields are always valid. Theme manifest declared keys are added on top.
130+
131+
---
132+
133+
## Key Files
134+
135+
- **New**: `src/rockgarden/validation.py`
136+
- **Modified**: `src/rockgarden/cli.py` — add `validate` command
137+
- **New (per-theme, optional)**: `_themes/<name>/theme.toml`
138+
139+
---
140+
141+
## Verification
142+
143+
- `rockgarden validate` on a clean config → exits 0, "No issues found"
144+
- Introduce a typo (`titl = "foo"`) → warning reported
145+
- Set `timezone = "Bad/Zone"` → error reported, exit 1
146+
- Set `theme.name` to nonexistent dir → warning reported
147+
- Create a theme manifest with a required key, omit it from TOML → warning reported
148+
- `uv run pytest` still passes (no regressions)

plans/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ The [PyOhio static website](https://github.com/pyohio/static-website) (Astro + P
4141
| N8 | Tag Index Pages || B | Generate `/tags/<tag>/` listing pages |
4242
| N9 | Template Decomposition || A | Named blocks as customization hooks in page templates |
4343
| N10 | Newline Handling || A | Obsidian-style single newline → `<br>` |
44+
| N11 | [Config Validation](N11-config-validation.md) || B | `validate` command, unknown-key warnings, theme manifest |
4445

4546
## Roadmap Phases
4647

plans/implementation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ After each step, verify incrementally:
195195
- [ ] **Graph View**: Interactive visualization of page connections
196196
- Requirements to be workshopped and defined
197197

198+
- [ ] Config validation command + theme manifest (N11)
198199
- [ ] Collections and content models (Feature 14)
199200
- [ ] Build hooks (Feature 15)
200201
- [ ] Base path prefix support (Feature 12)

src/rockgarden/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class SiteConfig:
1414
output: Path = field(default_factory=lambda: Path("_site"))
1515
clean_urls: bool = True
1616
base_url: str = ""
17-
timezone: str = "UTC"
1817

1918

2019
@dataclass
@@ -87,7 +86,8 @@ class DatesConfig:
8786
created_date_fields: list[str] = field(
8887
default_factory=lambda: ["created", "date", "date_created"]
8988
)
90-
modified_date_fallback: bool = True
89+
modified_date_fallback: bool = False
90+
timezone: str = "UTC"
9191

9292

9393
@dataclass
@@ -141,7 +141,6 @@ def from_dict(cls, data: dict) -> "Config":
141141
output=Path(site_data.get("output", "_site")),
142142
clean_urls=site_data.get("clean_urls", True),
143143
base_url=site_data.get("base_url", "").rstrip("/"),
144-
timezone=site_data.get("timezone", "UTC"),
145144
)
146145

147146
icons_dir_raw = build_data.get("icons_dir")
@@ -191,8 +190,9 @@ def from_dict(cls, data: dict) -> "Config":
191190
dates_defaults.created_date_fields,
192191
),
193192
modified_date_fallback=dates_data.get(
194-
"modified_date_fallback", True
193+
"modified_date_fallback", False
195194
),
195+
timezone=dates_data.get("timezone", "UTC"),
196196
)
197197

198198
return cls(

0 commit comments

Comments
 (0)