#!/usr/bin/env python3 """Static site generator for SciSiteForge.""" from __future__ import annotations import argparse from pathlib import Path import sys from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from scisiteforge.config import DEFAULT_THEME, load_config, save_config from scisiteforge.content import ( SiteContent, cards_from_config, load_citegeist_cards, load_didactopus_cards, load_doclift_cards, load_groundrecall_cards, ) from scisiteforge.render import html_escape, read_text, render_template, write_text from scisiteforge.notebook import load_notebooks, render_notebooks from scisiteforge.themes import available_themes, get_theme, materialize_theme def _prompt_for_config() -> dict[str, Any]: print("=== SciSiteForge Site Config ===") themes = available_themes() print("Available themes:") for theme in themes: print(f" - {theme.name}: {theme.description}") theme_name = input(f"Theme name (default: {DEFAULT_THEME}): ").strip() or DEFAULT_THEME languages_input = input("Languages (code:name pairs, comma-separated; default: en:English): ").strip() or "en:English" languages = [] for pair in languages_input.split(","): code, name = pair.strip().split(":", 1) languages.append({"code": code.strip(), "name": name.strip()}) return { "lang": input("Language code (default: en): ").strip() or "en", "title": input("Page title (default: SciSiteForge Preview): ").strip() or "SciSiteForge Preview", "site_title": input("Site name (default: SciSiteForge): ").strip() or "SciSiteForge", "license": input("License text (default: CC BY-SA 4.0): ").strip() or "CC BY-SA 4.0", "github_url": input("GitHub URL (optional): ").strip() or "https://github.com/", "contact_email": input("Contact email (optional): ").strip() or "admin@example.org", "theme": theme_name, "languages": languages, "navigation": [ {"label": "Home", "href": "/"}, ], "hero": { "kicker": "Preview", "title": "A site shell that can adapt to more than one audience.", "lede": "SciSiteForge now supports multiple theme presets and local content loaders for reusable science sites.", "actions": [ {"label": "Read the overview", "href": "#overview", "primary": True}, {"label": "Theme catalog", "href": "#themes", "primary": False}, ], }, "content_sources": {}, "content": {}, "notebooks": [], } def _language_options_html(languages: list[dict[str, str]], current_lang: str) -> str: visible_languages = [ item for item in languages if item.get("coverage", True) or item.get("code") == current_lang ] return "\n".join( f'' for item in visible_languages ) def _language_policy_html(language_policy: dict[str, Any]) -> str: planned_languages = language_policy.get("planned_languages", []) if not planned_languages: return "" planned_names = ", ".join(html_escape(item.get("name", item.get("code", ""))) for item in planned_languages if item.get("name") or item.get("code")) if not planned_names: return "" return f'

Planned languages: {planned_names}

' def _hero_actions_html(actions: list[dict[str, Any]]) -> str: if not actions: return "" return "\n".join( f'{html_escape(action.get("label", "Open"))}' for action in actions ) def _navigation_html(navigation: list[dict[str, str]]) -> str: return "\n".join( f'{html_escape(item.get("label", "Link"))}' for item in navigation ) def _render_cards(cards: list, template_path: str | Path, lang: str) -> str: if not cards: return "" template = read_text(template_path) rendered: list[str] = [] for card in cards: rendered.append( render_template( template, { "lang": lang, "app_title": html_escape(card.title), "app_description": html_escape(card.body), "app_slug": html_escape(card.source or card.title.lower().replace(" ", "-")), "section_title": html_escape(card.title), "section_meta": html_escape(card.meta), "section_excerpt": html_escape(card.body), "section_path": html_escape(card.source or card.title.lower().replace(" ", "-")), "href": html_escape(card.href), "link_label": "Open", }, ) ) return "\n".join(rendered) def build_site(config_file: str | Path, output_dir: str | Path) -> dict[str, Any]: config = load_config(config_file) theme = get_theme(config.get("theme")) out_path = Path(output_dir) out_path.mkdir(parents=True, exist_ok=True) theme_context = materialize_theme(theme, out_path) template = read_text(theme.template_path) content_sources = config.get("content_sources", {}) site_content = SiteContent() if source := content_sources.get("doclift_bundle"): site_content.section_cards.extend(load_doclift_cards(source)) if source := content_sources.get("groundrecall_bundle"): site_content.section_cards.extend(load_groundrecall_cards(source)) if source := content_sources.get("didactopus_pack"): site_content.feature_cards.extend(load_didactopus_cards(source)) if source := content_sources.get("bibliography"): site_content.bibliography_entries.extend(load_citegeist_cards(source)) inline_content = config.get("content", {}) site_content.feature_cards.extend(cards_from_config(inline_content.get("feature_cards", []), default_kind="feature")) site_content.section_cards.extend(cards_from_config(inline_content.get("section_cards", []), default_kind="section")) site_content.app_cards.extend(cards_from_config(inline_content.get("app_cards", []), default_kind="app")) site_content.bibliography_entries.extend(cards_from_config(inline_content.get("bibliography_entries", []), default_kind="bibliography")) notebooks = load_notebooks(config) languages = config.get("languages", [{"code": config.get("lang", "en"), "name": "English", "coverage": True}]) language_policy = config.get("language_policy", {}) hero = config.get("hero", {}) page_context = { "lang": config.get("lang", "en"), "page_title": html_escape(config.get("title", config.get("site_title", "SciSiteForge"))), "site_title": html_escape(config.get("site_title", "SciSiteForge")), "description": html_escape(config.get("description", "")), "license": html_escape(config.get("license", "CC BY-SA 4.0")), "github_url": html_escape(config.get("github_url", "")), "contact_email": html_escape(config.get("contact_email", "")), "theme_name": html_escape(theme.name), "theme_display_name": html_escape(theme.display_name), "theme_description": html_escape(theme.description), "theme_stylesheet_href": theme_context["theme_stylesheet_href"], "theme_script_href": theme_context["theme_script_href"], "theme_asset_prefix": theme_context["theme_asset_prefix"], "body_class": html_escape(theme.body_class), "site_shell_class": html_escape(theme.shell_class), "page_class": html_escape(theme.page_class), "navigation_html": _navigation_html(config.get("navigation", [])), "language_options": _language_options_html(languages, config.get("lang", "en")), "language_policy_html": _language_policy_html(language_policy), "hero_kicker": html_escape(hero.get("kicker", theme.display_name)), "hero_title": html_escape(hero.get("title", config.get("title", ""))), "hero_lede": html_escape(hero.get("lede", config.get("description", ""))), "hero_actions_html": _hero_actions_html(hero.get("actions", [])), "feature_cards_html": _render_cards(site_content.feature_cards, Path(__file__).parent.parent / "templates" / "app-card.html", config.get("lang", "en")), "section_cards_html": _render_cards(site_content.section_cards, Path(__file__).parent.parent / "templates" / "notebook-section.html", config.get("lang", "en")), "app_cards_html": _render_cards(site_content.app_cards, Path(__file__).parent.parent / "templates" / "app-card.html", config.get("lang", "en")), "bibliography_html": "\n".join( f'
  • {html_escape(card.title)} {html_escape(card.body)}
  • ' for card in site_content.bibliography_entries ), "notebook_html": render_notebooks(notebooks, site_content), } page_context.update( { "content_panels_html": page_context["feature_cards_html"] + "\n" + page_context["section_cards_html"], } ) rendered = render_template(template, page_context) write_text(out_path / "index.html", rendered) return {"output_dir": str(out_path), "theme": theme.name, "theme_assets": theme_context["theme_assets"]} def main() -> None: parser = argparse.ArgumentParser(description="Build a SciSiteForge site from a JSON config.") parser.add_argument("--init", action="store_true", help="Create site config interactively") parser.add_argument("--config", help="Path to site.json") parser.add_argument("--output", help="Output directory for built site") parser.add_argument("--themes", action="store_true", help="List the built-in theme presets") parser.add_argument("--save-config", help="Where to write the config when using --init", default="site.json") args = parser.parse_args() if args.themes: for theme in available_themes(): print(f"{theme.name}: {theme.description}") return if args.init: config = _prompt_for_config() save_config(args.save_config, config) print(f"Wrote config to {args.save_config}") return if args.config and args.output: result = build_site(args.config, args.output) print(f"Built {result['output_dir']} with theme {result['theme']}") return parser.print_help() if __name__ == "__main__": main()