diff --git a/.claude/mkdocs_hooks.py b/.claude/mkdocs_hooks.py new file mode 100644 index 00000000..02f29b3d --- /dev/null +++ b/.claude/mkdocs_hooks.py @@ -0,0 +1,212 @@ +""" +MkDocs hooks for dynamic README.md discovery and navigation generation. +This hook discovers all README.md files in the htk directory structure, +creates symbolic links, and dynamically generates the navigation config. +""" + +from pathlib import Path + + +def get_title_from_readme(path: Path) -> str: + """Extract the first heading from a README file to use as title.""" + try: + with open(path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line.startswith('# '): + return line[2:].strip() + except Exception: + pass + return None + + +def format_name(dirname: str) -> str: + """Convert directory name to readable format.""" + # Convert snake_case to Title Case + words = dirname.replace('_', ' ').split() + return ' '.join(word.capitalize() for word in words) + + +def generate_nav_config(htk_base: Path) -> list: + """ + Generate complete mkdocs navigation structure from filesystem. + Scans for README.md files and builds nested navigation dynamically. + """ + nav_config = [] + + # Home + nav_config.append({'Home': 'index.md'}) + + # Top-level modules (direct children of htk/) + top_level_modules = [ + 'admin', 'admintools', 'api', 'cache', 'constants', + 'decorators', 'extensions', 'forms', 'middleware', 'models', + 'scripts', 'test_scaffold', 'templatetags', 'utils', 'validators' + ] + + for module in top_level_modules: + module_path = htk_base / module + if (module_path / 'README.md').exists(): + title = get_title_from_readme(module_path / 'README.md') + if not title: + title = format_name(module) + nav_config.append({title: f'{module}.md'}) + + # Django Apps with submenu + apps_path = htk_base / 'apps' + if apps_path.exists(): + apps_nav = [{'Overview': 'apps.md'}] + apps = sorted([ + d for d in apps_path.iterdir() + if d.is_dir() and not d.name.startswith(('_', '__')) + and (d / 'README.md').exists() + ]) + + for app_dir in apps: + title = get_title_from_readme(app_dir / 'README.md') + if not title: + title = format_name(app_dir.name) + apps_nav.append({title: f'apps/{app_dir.name}.md'}) + + nav_config.append({'Django Apps': apps_nav}) + + # Libraries with submenu + lib_path = htk_base / 'lib' + if lib_path.exists(): + libs_nav = [{'Overview': 'lib.md'}] + libs = sorted([ + d for d in lib_path.iterdir() + if d.is_dir() and not d.name.startswith(('_', '__')) + and (d / 'README.md').exists() + ]) + + for lib_dir in libs: + title = get_title_from_readme(lib_dir / 'README.md') + if not title: + title = format_name(lib_dir.name) + libs_nav.append({title: f'lib/{lib_dir.name}.md'}) + + nav_config.append({'Libraries': libs_nav}) + + return nav_config + + +def create_symlinks(docs_dir: Path, htk_base: Path): + """ + Create necessary symbolic links from docs directory to actual README.md files. + This allows mkdocs to find all READMEs dynamically. + """ + # Ensure docs/apps and docs/lib directories exist + (docs_dir / 'apps').mkdir(exist_ok=True) + (docs_dir / 'lib').mkdir(exist_ok=True) + + # Map of top-level directory names to documentation file names + # This creates symlinks like: admin.md -> ../admin/README.md + top_level_dirs = [ + 'admin', 'admintools', 'api', 'cache', 'constants', 'decorators', + 'extensions', 'forms', 'middleware', 'models', 'utils', 'validators', + 'test_scaffold', 'scripts', 'templatetags' + ] + + # Create symlinks for top-level modules + for module_name in top_level_dirs: + module_dir = htk_base / module_name + if module_dir.exists() and module_dir.is_dir(): + readme = module_dir / 'README.md' + if readme.exists(): + symlink = docs_dir / f'{module_name}.md' + try: + if symlink.exists() or symlink.is_symlink(): + symlink.unlink() + symlink.symlink_to(readme) + except Exception: + pass + + # Create symlink for index.md from main README.md + main_readme = htk_base / 'README.md' + if main_readme.exists(): + index_symlink = docs_dir / 'index.md' + try: + if index_symlink.exists() or index_symlink.is_symlink(): + index_symlink.unlink() + index_symlink.symlink_to(main_readme) + except Exception: + pass + + # Create symlinks for apps.md and lib.md overview files + apps_readme = htk_base / 'apps' / 'README.md' + if apps_readme.exists(): + apps_symlink = docs_dir / 'apps.md' + try: + if apps_symlink.exists() or apps_symlink.is_symlink(): + apps_symlink.unlink() + apps_symlink.symlink_to(apps_readme) + except Exception: + pass + + lib_readme = htk_base / 'lib' / 'README.md' + if lib_readme.exists(): + lib_symlink = docs_dir / 'lib.md' + try: + if lib_symlink.exists() or lib_symlink.is_symlink(): + lib_symlink.unlink() + lib_symlink.symlink_to(lib_readme) + except Exception: + pass + + # Clean up old directory-level README.md symlinks that shouldn't exist + # (We use top-level .md symlinks instead) + old_dirs = ['api', 'cache', 'decorators', 'forms', 'middleware', 'models', 'utils', 'validators'] + for dir_name in old_dirs: + old_readme = docs_dir / dir_name / 'README.md' + if old_readme.exists() or old_readme.is_symlink(): + try: + old_readme.unlink() + except Exception: + pass + + # Create symlinks for apps + apps_dir = htk_base / 'apps' + if apps_dir.exists(): + # Create symlinks for individual apps + for app_path in sorted(apps_dir.iterdir()): + if app_path.is_dir() and not app_path.name.startswith(('_', '__', 'README')): + readme = app_path / 'README.md' + if readme.exists(): + symlink = docs_dir / 'apps' / f'{app_path.name}.md' + try: + if symlink.exists() or symlink.is_symlink(): + symlink.unlink() + symlink.symlink_to(readme) + except Exception: + pass + + # Create symlinks for libraries + lib_dir = htk_base / 'lib' + if lib_dir.exists(): + for lib_path in sorted(lib_dir.iterdir()): + if lib_path.is_dir() and not lib_path.name.startswith(('_', '__', 'README')): + readme = lib_path / 'README.md' + if readme.exists(): + symlink = docs_dir / 'lib' / f'{lib_path.name}.md' + try: + if symlink.exists() or symlink.is_symlink(): + symlink.unlink() + symlink.symlink_to(readme) + except Exception: + pass + + +def on_pre_build(config, **kwargs): + """ + Pre-build hook to create symlinks and dynamically generate navigation. + This ensures mkdocs has access to all README.md files and the nav is always in sync. + """ + docs_dir = Path(config['docs_dir']) + htk_base = docs_dir.parent + + # Create all necessary symlinks + create_symlinks(docs_dir, htk_base) + + # Generate and set dynamic navigation config + config['nav'] = generate_nav_config(htk_base) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..0317e1f5 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,39 @@ +name: Build and Deploy Documentation + +on: + push: + branches: + - master + - main + paths: + - 'README.md' + - '*/README.md' + - 'mkdocs.yml' + - 'docs/**' + - '.github/workflows/deploy-docs.yml' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install mkdocs mkdocs-material mkdocs-include-markdown-plugin + + - name: Build documentation + run: | + mkdocs build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site diff --git a/.gitignore b/.gitignore index 0d20b648..83b0c4c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ *.pyc + +# MkDocs generated files +# The docs/ folder is generated at build time by the hook +docs/ +# The site/ folder is the HTML build output +site/ diff --git a/docs/static/css/mkdocs.css b/docs/static/css/mkdocs.css new file mode 100644 index 00000000..1820f7ef --- /dev/null +++ b/docs/static/css/mkdocs.css @@ -0,0 +1,66 @@ +/* Custom color scheme */ +:root { + --md-primary-fg-color: #0000e6; + --md-primary-fg-color-light: #1a1aff; + --md-primary-fg-color-dark: #0000b3; +} + +/* Disable bounce/rubber band effect on entire page while allowing scroll */ +html, +body { + overscroll-behavior: none; +} + +/* Disable pull-to-refresh and overscroll bounce on mobile */ +* { + overscroll-behavior: none; +} + +/* Disable footer bounce/animation on scroll */ +.md-footer { + position: relative !important; + animation: none !important; +} + +/* Ensure footer stays fixed and doesn't bounce */ +.md-footer__inner { + animation: none !important; +} + +/* Remove any transition animations on footer */ +.md-footer, +.md-footer__inner { + transition: none !important; +} + +/* Disable header bounce */ +.md-header { + animation: none !important; + transition: none !important; +} + +/* Hide offline plugin messages and skip links that appear below footer */ +.md-skip, +.md-announce, +[data-md-component="announce"] { + display: none !important; +} + +/* Ensure page doesn't have overflow issues */ +html { + overflow-x: hidden; +} + +/* Ensure footer is the last visible element */ +body { + display: flex; + flex-direction: column; + min-height: 100vh; + overflow-x: hidden; +} + +.md-container { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..d1e36bb0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,83 @@ +site_name: Django HTK (Hacktoolkit) +site_description: Production-ready Django toolkit with 29 apps, 47+ integrations, and 24 utility categories +site_author: Jonathan Tsai & HTK Contributors +site_url: https://hacktoolkit.github.io/django-htk/ +repo_url: https://github.com/hacktoolkit/django-htk +repo_name: django-htk +copyright: Copyright © 2025 HTK Contributors. MIT License. + +theme: + name: material + logo: https://www.hacktoolkit.com/logo.png + palette: + - scheme: default + primary: custom + accent: deep orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: custom + accent: deep orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + features: + - navigation.instant + - navigation.tracking + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + +extra_css: + - static/css/mkdocs.css + +plugins: + - search + +hooks: + - .claude/mkdocs_hooks.py + +markdown_extensions: + - admonition + - pymdownx.arithmatex + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.critic + - pymdownx.details + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + - codehilite + - footnotes + - tables + - toc: + permalink: true + +docs_dir: docs +site_dir: site +exclude_docs: | + *.py + __pycache__/ + venv/ + site/ + .git/ + .github/ + .claude/ + .venv/ + south_migrations/ + migrations/ + templates/ + __init__.py diff --git a/scripts/generate_nav.py b/scripts/generate_nav.py new file mode 100644 index 00000000..5cc883e8 --- /dev/null +++ b/scripts/generate_nav.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +""" +Generate mkdocs navigation from README.md files in the htk directory structure. +This script scans the directory for README.md files and generates the appropriate +mkdocs navigation configuration dynamically. +""" + +import os +import sys +from pathlib import Path +from typing import Dict, List, Any + +def get_title_from_readme(path: Path) -> str: + """Extract the first heading from a README file to use as title.""" + try: + with open(path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line.startswith('# '): + return line[2:].strip() + except Exception: + pass + return None + +def format_name(dirname: str) -> str: + """Convert directory name to readable format.""" + # Convert snake_case to Title Case + words = dirname.replace('_', ' ').split() + return ' '.join(word.capitalize() for word in words) + +def build_nav_from_filesystem(base_path: Path, nav: List[Dict[str, Any]] = None, is_root: bool = True) -> List[Dict[str, Any]]: + """ + Recursively build navigation from filesystem structure. + Looks for README.md files and builds nested navigation. + """ + if nav is None: + nav = [] + + # First, add standalone README.md files in this directory + readme_path = base_path / 'README.md' + if readme_path.exists() and not is_root: + title = get_title_from_readme(readme_path) + if not title: + title = format_name(base_path.name) + + # Calculate relative path from docs directory + try: + rel_path = readme_path.relative_to(base_path.parent) + # Convert to docs format: ../module/README.md -> module.md + nav_path = str(rel_path).replace('README.md', '').rstrip('/') + '.md' + nav_path = nav_path.replace('/', '') + + if nav_path: + nav.append({title: nav_path}) + except ValueError: + pass + + # Then, process subdirectories with their own README.md files + subdirs = [] + try: + for item in sorted(base_path.iterdir()): + if item.is_dir() and not item.name.startswith(('.', '_', 'venv', '__pycache__', 'migrations', 'south_migrations', 'static', 'templates')): + readme_exists = (item / 'README.md').exists() + if readme_exists: + subdirs.append(item) + except PermissionError: + pass + + # Add subdirectories + for subdir in subdirs: + title = get_title_from_readme(subdir / 'README.md') + if not title: + title = format_name(subdir.name) + + rel_path = subdir / 'README.md' + nav_path = f"{subdir.name}.md" + nav.append({title: nav_path}) + + return nav + +def generate_mkdocs_nav(htk_base: Path) -> Dict[str, Any]: + """Generate complete mkdocs navigation structure.""" + nav_config = [] + + # Home + nav_config.append({'Home': 'index.md'}) + + # Top-level modules (direct children of htk/) + top_level_modules = [ + 'admin', 'admintools', 'api', 'cache', 'constants', 'data', + 'decorators', 'extensions', 'forms', 'middleware', 'models', + 'scripts', 'test_scaffold', 'templatetags', 'utils', 'validators' + ] + + for module in top_level_modules: + module_path = htk_base / module + if (module_path / 'README.md').exists(): + title = get_title_from_readme(module_path / 'README.md') + if not title: + title = format_name(module) + nav_config.append({title: f'{module}.md'}) + + # Django Apps with submenu + apps_path = htk_base / 'apps' + if apps_path.exists(): + apps_nav = [{'Overview': 'apps.md'}] + apps = sorted([d for d in apps_path.iterdir() if d.is_dir() and not d.name.startswith(('_', '__')) and (d / 'README.md').exists()]) + + for app_dir in apps: + title = get_title_from_readme(app_dir / 'README.md') + if not title: + title = format_name(app_dir.name) + apps_nav.append({title: f'apps/{app_dir.name}.md'}) + + nav_config.append({'Django Apps': apps_nav}) + + # Libraries with submenu + lib_path = htk_base / 'lib' + if lib_path.exists(): + libs_nav = [{'Overview': 'lib.md'}] + libs = sorted([d for d in lib_path.iterdir() if d.is_dir() and not d.name.startswith(('_', '__')) and (d / 'README.md').exists()]) + + for lib_dir in libs: + title = get_title_from_readme(lib_dir / 'README.md') + if not title: + title = format_name(lib_dir.name) + libs_nav.append({title: f'lib/{lib_dir.name}.md'}) + + nav_config.append({'Libraries': libs_nav}) + + return nav_config + +if __name__ == '__main__': + # Find the htk directory + script_dir = Path(__file__).parent + htk_dir = script_dir.parent + + nav = generate_mkdocs_nav(htk_dir) + + # Print as YAML-like format for verification + print("Navigation structure:") + print("=" * 60) + for i, item in enumerate(nav, 1): + print(f"{i}. {list(item.keys())[0]}") + + print("\n✓ Navigation generated successfully") + print(f" Total items: {len(nav)}") + sys.exit(0) diff --git a/static/htk/images/logo/logo.png b/static/htk/images/logo/logo.png deleted file mode 100644 index dd234aaf..00000000 Binary files a/static/htk/images/logo/logo.png and /dev/null differ