diff --git a/Dockerfile b/Dockerfile index 210c0bf..2cd4aa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,4 +66,9 @@ RUN hugo --destination /workspace/public EXPOSE 1313 -CMD ["hugo", "server", "--bind", "0.0.0.0", "--baseURL", "https://bazel-docs-68tmf.ondigitalocean.app/", "--disableFastRender"] +# Default base URL can be overridden at build or run time +ARG BASE_URL=https://bazel-docs-68tmf.ondigitalocean.app/ +ENV HUGO_BASEURL=${BASE_URL} + +# Use shell form so the environment variable is expanded correctly +CMD ["sh","-c", "hugo server --bind 0.0.0.0 --baseURL \"$HUGO_BASEURL\" --disableFastRender"] diff --git a/config.yaml b/config.yaml index f690cc5..85e7dee 100644 --- a/config.yaml +++ b/config.yaml @@ -1,11 +1,10 @@ # Configuration for Devsite to Hugo Converter -# Source repository settings +# Docsy Theme Configuration source_repo: - owner: "bazelbuild" - name: "bazel" - branch: "master" - path: "site/en" + owner: "alan707" + name: "bazel-docs" + branch: "main" # Hugo site settings hugo: @@ -15,47 +14,103 @@ hugo: languageCode: "en-us" theme: "docsy" -# Content mapping - map Devsite sections to Hugo content types +# Content mapping - organized into 4 main categories content_mapping: - concepts: - type: "docs" + # TUTORIALS - Step-by-step guides for users + tutorials: + type: "tutorials" weight: 10 - basics: - type: "docs" + category: "Tutorials" + start: + type: "tutorials" weight: 20 - tutorials: - type: "docs" + category: "Tutorials" + basics: + type: "tutorials" weight: 30 + category: "Tutorials" + + # HOW-TO GUIDES - Practical instructions for specific tasks + install: + type: "how-to-guides" + weight: 10 + category: "How-To Guides" + configure: + type: "how-to-guides" + weight: 20 + category: "How-To Guides" build: - type: "docs" + type: "how-to-guides" + weight: 30 + category: "How-To Guides" + run: + type: "how-to-guides" weight: 40 - configure: - type: "docs" + category: "How-To Guides" + remote: + type: "how-to-guides" weight: 50 - extending: - type: "docs" + category: "How-To Guides" + migrate: + type: "how-to-guides" weight: 60 - external: - type: "docs" + category: "How-To Guides" + rules: + type: "how-to-guides" weight: 70 - remote: - type: "docs" + category: "How-To Guides" + docs: + type: "how-to-guides" weight: 80 - query: - type: "docs" - weight: 90 - reference: - type: "docs" - weight: 100 + category: "How-To Guides" + + # EXPLANATIONS - In-depth articles explaining concepts and features + concepts: + type: "explanations" + weight: 10 + category: "Explanations" + extending: + type: "explanations" + weight: 20 + category: "Explanations" + external: + type: "explanations" + weight: 30 + category: "Explanations" + about: + type: "explanations" + weight: 40 + category: "Explanations" community: - type: "community" - weight: 200 + type: "explanations" + weight: 50 + category: "Explanations" contribute: - type: "community" - weight: 210 - about: - type: "about" - weight: 300 + type: "explanations" + weight: 60 + category: "Explanations" + release: + type: "explanations" + weight: 70 + category: "Explanations" + + # REFERENCE - Detailed reference material for advanced users + reference: + type: "reference" + weight: 10 + category: "Reference" + query: + type: "reference" + weight: 20 + category: "Reference" + versions: + type: "reference" + weight: 30 + category: "Reference" + advanced: + type: "reference" + weight: 40 + category: "Reference" # CSS conversion settings css_conversion: @@ -99,3 +154,156 @@ file_patterns: - "*.bzl" - ".git/**" - "node_modules/**" + +# Used to automatically detect language for code blocks without explicit language identifiers +code_language_patterns: + starlark: + - "cc_binary(" + - "cc_library(" + - "java_library(" + - "py_binary(" + - "py_library(" + - "load(" + - "BUILD" + - "WORKSPACE" + - "bazel-" + - "name =" + - "srcs =" + - "deps =" + - "cc_toolchain(" + - "toolchain(" + - "filegroup(" + - "exports_files(" + bash: + - "bazel " + - "bazel help" + - "bazel build" + - "bazel test" + - "bazel run" + - "bazel query" + - "#!/bin/bash" + - "#!/bin/sh" + - "echo " + - "export " + - "source " + - "$(" + - "${" + - "$" + - "npm " + - "yarn " + - "git " + - "cd " + - "mkdir " + - "rm " + - "cp " + python: + - "def " + - "class " + - "import " + - "from " + - "print(" + - "__init__" + - "self." + - "@" + - "lambda " + - "return " + - "if __name__" + - "try:" + - "except:" + cpp: + - "#include" + - "int main" + - "std::" + - "namespace " + - "cout <<" + - "void " + - "template<" + - "nullptr" + - ".cc" + - ".cpp" + - ".h" + - "cin >>" + - "using namespace" + - "printf(" + - "scanf(" + java: + - "public class" + - "private " + - "protected " + - "import java" + - "package " + - "extends " + - "implements " + - "@Override" + - "System.out.println" + - "new " + - "throw new" + - "catch (" + javascript: + - "function " + - "const " + - "let " + - "var " + - "=>" + - "console.log" + - "require(" + - "export " + - "import " + - "async " + - "await " + - "document." + - "window." + - "addEventListener" + typescript: + - "interface " + - "type " + - ": string" + - ": number" + - ": boolean" + - "enum " + - "readonly " + - "as " + yaml: + - "name:" + - "type:" + - "- name:" + - "kind:" + - "apiVersion:" + - "metadata:" + - "spec:" + json: + - '{"' + - '":' + - '": ' + - '[' + - '],' + xml: + - "" + - "xmlns" + html: + - "" + - " Dict: """Load configuration from YAML file""" @@ -92,15 +90,13 @@ def convert_documentation(self, devsite_structure, source_path, output_path, dry_run, incremental) - # Convert CSS and static assets + # Convert static assets if not dry_run: self._convert_assets(source_path, output_path) # Generate Hugo configuration if not dry_run: - self._generate_hugo_config(devsite_structure, output_path) - # Skip custom layouts when using Docsy theme - # self._generate_layouts(output_path) + self._generate_hugo_config(output_path) self._generate_section_indices(devsite_structure, output_path) logger.info(f"Conversion completed successfully") @@ -172,9 +168,12 @@ def _convert_content_files(self, devsite_structure: Dict, source_path: str, conversion_stats['skipped_files'] += 1 continue + # Determine category for this file + category_path = self._get_category_path(relative_path) + # Convert file if self._convert_single_file( - md_file, output_dir / 'content' / relative_path, + md_file, output_dir / 'content' / category_path, devsite_structure, dry_run): conversion_stats['converted_files'] += 1 else: @@ -186,6 +185,26 @@ def _convert_content_files(self, devsite_structure: Dict, source_path: str, return conversion_stats + def _get_category_path(self, relative_path: Path) -> Path: + """Get the category path for a file based on its section""" + # Get the top-level directory (section) + parts = relative_path.parts + if not parts: + return relative_path + + section_name = parts[0] + + # Look up the category for this section + if section_name in self.config['content_mapping']: + mapping = self.config['content_mapping'][section_name] + category_type = mapping['type'] + + # Return path with category prefix + return Path(category_type) / relative_path + + # Default to original path if no mapping found + return relative_path + def _convert_single_file(self, source_file: Path, output_file: Path, devsite_structure: Dict, dry_run: bool) -> bool: """Convert a single markdown file from Devsite to Hugo format""" @@ -457,55 +476,134 @@ def fix_inline_tree(match): return content def _add_language_identifiers_to_code_blocks(self, content: str) -> str: - """Add language identifiers to code blocks without them to prevent KaTeX rendering issues""" - - def determine_language(code_content): - """Determine appropriate language identifier based on code content""" - code_lower = code_content.lower().strip() - - # Check for common patterns - if 'load(' in code_content or 'cc_library(' in code_content or 'java_library(' in code_content: - return 'starlark' # Bazel/Starlark (was incorrectly 'python') - elif code_content.startswith('/') or 'BUILD' in code_content: - return 'text' # File paths and directory structures - elif any(keyword in code_lower - for keyword in ['def ', 'class ', 'import ', 'from ']): - return 'python' - elif any(keyword in code_lower - for keyword in ['function', 'var ', 'const ', 'let ']): - return 'javascript' - elif any(keyword in code_lower - for keyword in ['#include', 'int main', 'std::']): - return 'cpp' - elif any(keyword in code_lower - for keyword in ['public class', 'import java']): - return 'java' - elif '$' in code_content or 'echo' in code_lower or code_content.startswith( - '#!/'): - return 'bash' - else: - return 'text' # Default for unknown content - - # Clean up any text that appears after closing backticks - # This ensures nothing ever appears after ``` - content = re.sub(r'```[^\n\r]*(\n|$)', '```\n', content) - - # Pattern to match code blocks that start with ``` followed by only whitespace and newline - # This ensures we only match code blocks WITHOUT language identifiers - pattern = r'```\s*\n(.*?)\n```' - - def replace_code_block(match): - code_content = match.group(1) - language = determine_language(code_content) - return f'```{language}\n{code_content}\n```' - - # Apply the replacement using multiline and dotall flags - result = re.sub(pattern, - replace_code_block, - content, - flags=re.MULTILINE | re.DOTALL) - - return result + """Annotate unlabeled code fences with language identifiers. + + + Parameters + ---------- + content : str + The markdown content possibly containing fenced code blocks. + + Returns + ------- + str + Content with unlabeled code blocks annotated with language + identifiers. + """ + # Fetch language detection patterns from configuration, or use + # sensible defaults. Keys are language names, values are + # sequences of substrings indicative of that language. + language_patterns: Dict[str, List[str]] = self.config.get('code_language_patterns') + + def determine_language(code_content: str) -> str: + """Return the best language guess for a code snippet. + + This helper inspects the provided code content for the + presence of known substrings. The first matching language + wins. If no patterns match, ``text`` is returned. + + Parameters + ---------- + code_content : str + Raw code content extracted from a fenced code block. + + Returns + ------- + str + Name of the detected language or ``'text'`` when + uncertain. + """ + if not code_content or not code_content.strip(): + return 'text' + # Treat directory structures as plain text (these may use + # ASCII/Unicode tree characters or start with a leading slash). + stripped_content = code_content.strip() + if stripped_content.startswith('/') or any( + char in code_content for char in ('└', '├', '│') + ): + return 'text' + # Check each configured language pattern in the order they + # appear. The first match determines the language. + for language, patterns in language_patterns.items(): + for pattern in patterns: + if pattern in code_content: + return language + return 'text' + + # Split the content into lines, preserving line endings so we + # can reconstruct the document accurately. + lines: List[str] = content.splitlines(keepends=True) + result: List[str] = [] + in_code_block = False + in_unlabeled_block = False + code_lines: List[str] = [] + fence_indent: str = '' + + idx = 0 + while idx < len(lines): + line = lines[idx] + stripped = line.strip() + # Detect the start of a code fence when not already in a block + if not in_code_block: + if stripped.startswith('```'): + after = stripped[3:].strip() + # Capture the indent of the fence (everything before + # the first non-whitespace character). This indent is + # reused when emitting annotated fences to preserve + # formatting inside lists. + fence_indent = line[: len(line) - len(line.lstrip())] + if after == '': + # Begin an unlabeled fenced code block + in_code_block = True + in_unlabeled_block = True + code_lines = [] + else: + # Begin a labeled fenced code block; copy the opening + # fence verbatim + in_code_block = True + in_unlabeled_block = False + result.append(line) + else: + result.append(line) + else: + # We're inside a fenced block + if stripped.startswith('```'): + # Encountered a closing fence + if in_unlabeled_block: + # Compute the language and emit annotated fence + code_content = ''.join(code_lines).rstrip('\n\r') + language = determine_language(code_content) + result.append(f'{fence_indent}```{language}\n') + if code_content: + result.append(code_content + '\n') + result.append(f'{fence_indent}```\n') + # Reset state + code_lines = [] + in_unlabeled_block = False + in_code_block = False + else: + # Labeled block; preserve closing fence + result.append(line) + in_code_block = False + else: + # Collect lines inside the fenced block + if in_unlabeled_block: + code_lines.append(line) + else: + result.append(line) + idx += 1 + + # Handle unterminated unlabeled blocks at EOF. If the file ends + # inside an unlabeled code block, annotate it anyway. + if in_code_block and in_unlabeled_block: + code_content = ''.join(code_lines).rstrip('\n\r') + language = determine_language(code_content) + result.append(f'{fence_indent}```{language}\n') + if code_content: + result.append(code_content + '\n') + result.append(f'{fence_indent}```\n') + + return ''.join(result) def _generate_hugo_markdown(self, frontmatter: Dict, body: str) -> str: """Generate Hugo markdown file with frontmatter and body""" @@ -524,16 +622,10 @@ def _file_needs_conversion(self, source_file: Path, return source_file.stat().st_mtime > output_file.stat().st_mtime def _convert_assets(self, source_path: str, output_path: str) -> None: - """Convert CSS and static assets""" + """Convert static assets""" source_dir = Path(source_path) output_dir = Path(output_path) - # Convert CSS files - css_files = list(source_dir.rglob('*.css')) - for css_file in css_files: - self.css_converter.convert_css_file(css_file, - output_dir / 'assets' / 'scss') - # Copy static assets (images, etc.) static_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'] for ext in static_extensions: @@ -544,10 +636,9 @@ def _convert_assets(self, source_path: str, output_path: str) -> None: shutil.copy2(asset_file, output_asset) logger.debug(f"Copied asset: {asset_file} -> {output_asset}") - def _generate_hugo_config(self, devsite_structure: Dict, - output_path: str) -> None: + def _generate_hugo_config(self, output_path: str) -> None: """Generate Hugo configuration file""" - self.hugo_generator.generate_config(devsite_structure, output_path) + self.hugo_generator.generate_config(output_path) def _generate_layouts(self, output_path: str) -> None: """Generate Hugo layout templates""" diff --git a/templates/hugo_config.yaml.jinja2 b/templates/hugo_config.yaml.jinja2 index 009eece..33c3253 100644 --- a/templates/hugo_config.yaml.jinja2 +++ b/templates/hugo_config.yaml.jinja2 @@ -1,10 +1,10 @@ # Hugo configuration for Bazel documentation site # Generated automatically by devsite-to-hugo-converter -baseURL: "{{ config.baseURL }}" -title: "{{ config.title }}" -description: "{{ config.description }}" -languageCode: "{{ config.languageCode }}" +baseURL: "{{ hugo.baseURL }}" +title: "{{ hugo.title }}" +description: "{{ hugo.description }}" +languageCode: "{{ hugo.languageCode }}" # Content configuration contentDir: "content" @@ -13,6 +13,13 @@ layoutDir: "layouts" staticDir: "static" archetypeDir: "archetypes" +# Cache configuration +caches: + images: + dir: :cacheDir/images + +math: false + # Build configuration buildDrafts: false buildFuture: false @@ -50,29 +57,37 @@ markup: # Menu configuration menu: main: - - name: "Documentation" - url: "/docs/" + - name: "Tutorials" + url: "/tutorials/" weight: 10 - - name: "Community" - url: "/community/" + - name: "How-To Guides" + url: "/how-to-guides/" weight: 20 - - name: "About" - url: "/about/" + - name: "Explanations" + url: "/explanations/" weight: 30 + - name: "Reference" + url: "/reference/" + weight: 40 + - name: "Community" + url: "/explanations/community/" + weight: 50 + - name: "About" + url: "/explanations/about/" + weight: 60 # Parameters for Docsy theme params: # Repository information github_repo: "https://github.com/{{ source_repo.owner }}/{{ source_repo.name }}" github_branch: "{{ source_repo.branch }}" - github_subdir: "{{ source_repo.path }}" # Edit page configuration edit_page: true # Search configuration search: - enabled: true + enabled: false # Navigation configuration navigation: @@ -80,13 +95,11 @@ params: # Footer configuration footer: - enable: true + enable: false # Disable math rendering katex: enable: false - math: - enable: false # Disable MathJax mathjax: enable: false @@ -101,6 +114,10 @@ params: ui: showLightDarkModeMenu: true mode: dark # Set the default mode to dark + sidebar_menu_compact: true + ul_show: 1 + sidebar_menu_foldable: true + sidebar_cache_limit: 1000 # Taxonomies taxonomies: @@ -109,9 +126,10 @@ taxonomies: # Output formats outputs: - home: ["HTML", "RSS", "JSON"] - page: ["HTML"] - section: ["HTML", "RSS"] + section: + - HTML + - RSS + # Security configuration security: diff --git a/utils/css_converter.py b/utils/css_converter.py deleted file mode 100644 index 357eecc..0000000 --- a/utils/css_converter.py +++ /dev/null @@ -1,283 +0,0 @@ -""" -CSS Converter Module -Handles conversion of CSS files to Hugo-compatible SCSS -""" - -import os -import re -import logging -from pathlib import Path -from typing import Dict, List, Optional - -logger = logging.getLogger(__name__) - -class CSSConverter: - """Converter for CSS files to Hugo-compatible SCSS""" - - def __init__(self, config: Dict): - """Initialize converter with configuration""" - self.config = config - self.conversion_config = config.get('css_conversion', {}) - - def convert_css_file(self, css_file: Path, output_dir: Path) -> bool: - """ - Convert a CSS file to SCSS format - - Args: - css_file: Path to source CSS file - output_dir: Output directory for SCSS files - - Returns: - True if successful, False otherwise - """ - try: - # Read source CSS - with open(css_file, 'r', encoding='utf-8') as f: - css_content = f.read() - - # Convert to SCSS - scss_content = self._convert_css_to_scss(css_content) - - # Determine output file path - output_file = output_dir / f"_{css_file.stem}.scss" - output_file.parent.mkdir(parents=True, exist_ok=True) - - # Write SCSS file - with open(output_file, 'w', encoding='utf-8') as f: - f.write(scss_content) - - logger.debug(f"Converted CSS to SCSS: {css_file} -> {output_file}") - return True - - except Exception as e: - logger.error(f"Failed to convert CSS file {css_file}: {e}") - return False - - def _convert_css_to_scss(self, css_content: str) -> str: - """Convert CSS content to SCSS format""" - scss_content = css_content - - # Add SCSS header comment - header = """// Converted from Devsite CSS to SCSS for Hugo/Docsy -// Generated automatically by devsite-to-hugo-converter - -""" - - # Convert CSS custom properties to SCSS variables if configured - if self.conversion_config.get('preserve_custom_properties', True): - scss_content = self._convert_custom_properties(scss_content) - - # Add vendor prefixes handling - scss_content = self._add_vendor_prefix_mixins(scss_content) - - # Convert color values to SCSS variables - scss_content = self._extract_color_variables(scss_content) - - # Convert font definitions to SCSS variables - scss_content = self._extract_font_variables(scss_content) - - # Add responsive breakpoint support - scss_content = self._add_responsive_mixins(scss_content) - - return header + scss_content - - def _convert_custom_properties(self, css_content: str) -> str: - """Convert CSS custom properties to SCSS variables""" - # Find all CSS custom properties - custom_prop_pattern = r'--([a-zA-Z0-9-]+):\s*([^;]+);' - custom_props = re.findall(custom_prop_pattern, css_content) - - if not custom_props: - return css_content - - # Generate SCSS variables section - scss_vars = "\n// SCSS Variables (converted from CSS custom properties)\n" - for prop_name, prop_value in custom_props: - scss_var_name = prop_name.replace('-', '_') - scss_vars += f"${scss_var_name}: {prop_value.strip()};\n" - - # Replace CSS custom property usage with SCSS variables - modified_content = css_content - for prop_name, _ in custom_props: - css_var_usage = f"var(--{prop_name})" - scss_var_usage = f"${prop_name.replace('-', '_')}" - modified_content = modified_content.replace(css_var_usage, scss_var_usage) - - return scss_vars + "\n" + modified_content - - def _add_vendor_prefix_mixins(self, scss_content: str) -> str: - """Add SCSS mixins for vendor prefixes""" - mixins = """ -// Vendor prefix mixins -@mixin transform($value) { - -webkit-transform: $value; - -moz-transform: $value; - -ms-transform: $value; - transform: $value; -} - -@mixin transition($value) { - -webkit-transition: $value; - -moz-transition: $value; - -ms-transition: $value; - transition: $value; -} - -@mixin box-shadow($value) { - -webkit-box-shadow: $value; - -moz-box-shadow: $value; - box-shadow: $value; -} - -@mixin border-radius($value) { - -webkit-border-radius: $value; - -moz-border-radius: $value; - border-radius: $value; -} - -""" - return mixins + scss_content - - def _extract_color_variables(self, scss_content: str) -> str: - """Extract color values and convert to SCSS variables""" - # Find color values (hex, rgb, rgba, hsl, hsla) - color_patterns = [ - r'#[0-9a-fA-F]{3,6}', - r'rgb\([^)]+\)', - r'rgba\([^)]+\)', - r'hsl\([^)]+\)', - r'hsla\([^)]+\)' - ] - - colors = set() - for pattern in color_patterns: - colors.update(re.findall(pattern, scss_content)) - - if not colors: - return scss_content - - # Generate color variables - color_vars = "\n// Color variables\n" - color_map = {} - - for i, color in enumerate(sorted(colors)): - var_name = f"$color-{i + 1}" - color_vars += f"{var_name}: {color};\n" - color_map[color] = var_name - - # Replace color values with variables - modified_content = scss_content - for color, var_name in color_map.items(): - modified_content = modified_content.replace(color, var_name) - - return color_vars + "\n" + modified_content - - def _extract_font_variables(self, scss_content: str) -> str: - """Extract font definitions and convert to SCSS variables""" - # Find font-family declarations - font_pattern = r'font-family:\s*([^;]+);' - fonts = set(re.findall(font_pattern, scss_content)) - - if not fonts: - return scss_content - - # Generate font variables - font_vars = "\n// Font variables\n" - font_map = {} - - for i, font in enumerate(sorted(fonts)): - var_name = f"$font-family-{i + 1}" - font_vars += f"{var_name}: {font};\n" - font_map[font] = var_name - - # Replace font declarations with variables - modified_content = scss_content - for font, var_name in font_map.items(): - modified_content = modified_content.replace(f"font-family: {font};", f"font-family: {var_name};") - - return font_vars + "\n" + modified_content - - def _add_responsive_mixins(self, scss_content: str) -> str: - """Add responsive breakpoint mixins""" - responsive_mixins = """ -// Responsive breakpoint mixins -$breakpoints: ( - mobile: 576px, - tablet: 768px, - desktop: 992px, - large: 1200px -); - -@mixin respond-to($breakpoint) { - @if map-has-key($breakpoints, $breakpoint) { - @media (min-width: map-get($breakpoints, $breakpoint)) { - @content; - } - } @else { - @warn "Unknown breakpoint: #{$breakpoint}."; - } -} - -@mixin respond-below($breakpoint) { - @if map-has-key($breakpoints, $breakpoint) { - @media (max-width: map-get($breakpoints, $breakpoint) - 1px) { - @content; - } - } @else { - @warn "Unknown breakpoint: #{$breakpoint}."; - } -} - -""" - return responsive_mixins + scss_content - - def convert_devsite_styles(self, source_dir: Path, output_dir: Path) -> bool: - """ - Convert all Devsite CSS files to Hugo-compatible SCSS - - Args: - source_dir: Source directory containing CSS files - output_dir: Output directory for SCSS files - - Returns: - True if successful, False otherwise - """ - try: - css_files = list(source_dir.rglob('*.css')) - - if not css_files: - logger.info("No CSS files found to convert") - return True - - scss_dir = output_dir / 'scss' - scss_dir.mkdir(parents=True, exist_ok=True) - - success_count = 0 - for css_file in css_files: - if self.convert_css_file(css_file, scss_dir): - success_count += 1 - - # Generate main SCSS file that imports all converted files - self._generate_main_scss(scss_dir, css_files) - - logger.info(f"Converted {success_count}/{len(css_files)} CSS files to SCSS") - return success_count == len(css_files) - - except Exception as e: - logger.error(f"Failed to convert Devsite styles: {e}") - return False - - def _generate_main_scss(self, scss_dir: Path, css_files: List[Path]) -> None: - """Generate main SCSS file that imports all converted files""" - main_scss = "// Main SCSS file for Bazel documentation\n" - main_scss += "// Imports all converted Devsite CSS files\n\n" - - for css_file in css_files: - import_name = css_file.stem - main_scss += f"@import '{import_name}';\n" - - main_scss_file = scss_dir / '_main.scss' - with open(main_scss_file, 'w', encoding='utf-8') as f: - f.write(main_scss) - - logger.debug(f"Generated main SCSS file: {main_scss_file}") diff --git a/utils/hugo_generator.py b/utils/hugo_generator.py index 9995c82..bd37a45 100644 --- a/utils/hugo_generator.py +++ b/utils/hugo_generator.py @@ -12,6 +12,11 @@ logger = logging.getLogger(__name__) +TUTORIALS_DESCRIPTION = 'Tutorials to guide you through Bazel specific examples' +HOW_TO_GUIDES_DESCRIPTION = 'Guides for specific tasks and issues your will encounter' +EXPLANATIONS_DESCRIPTION = 'Understanding Bazel concepts and features' +REFERENCE_DESCRIPTION = 'Reference materials, API documentation, and good information for rules authors' + class HugoGenerator: """Generator for Hugo site structure and configuration""" @@ -20,7 +25,7 @@ def __init__(self, config: Dict): self.config = config self.template_env = Environment(loader=FileSystemLoader('templates')) - def generate_config(self, devsite_structure: Dict, output_path: str) -> bool: + def generate_config(self, output_path: str) -> bool: """ Generate Hugo configuration file @@ -33,14 +38,9 @@ def generate_config(self, devsite_structure: Dict, output_path: str) -> bool: """ try: output_dir = Path(output_path) - - # Prepare template context - context = { - 'config': self.config['hugo'], - 'source_repo': self.config['source_repo'], - 'devsite_structure': devsite_structure - } - + + with open(Path('config.yaml'), 'r') as f: + context = yaml.safe_load(f) # Render Hugo configuration template = self.template_env.get_template('hugo_config.yaml.jinja2') config_content = template.render(context) @@ -128,6 +128,34 @@ def _generate_main_index(self, content_dir: Path, devsite_structure: Dict) -> No 'weight': 1 } + # Create the 4 main categories + categories = [ + { + 'title': 'Tutorials', + 'path': '/tutorials/', + 'description': TUTORIALS_DESCRIPTION, + 'weight': 10 + }, + { + 'title': 'How-To Guides', + 'path': '/how-to-guides/', + 'description': HOW_TO_GUIDES_DESCRIPTION, + 'weight': 20 + }, + { + 'title': 'Explanations', + 'path': '/explanations/', + 'description': EXPLANATIONS_DESCRIPTION, + 'weight': 30 + }, + { + 'title': 'Reference', + 'path': '/reference/', + 'description': REFERENCE_DESCRIPTION, + 'weight': 40 + } + ] + # Render main index template = self.template_env.get_template('section_index.jinja2') index_content = template.render({ @@ -136,14 +164,7 @@ def _generate_main_index(self, content_dir: Path, devsite_structure: Dict) -> No 'description': context['description'], 'type': 'docs', 'weight': 1, - 'subsections': [ - { - 'title': section['title'], - 'path': f"/{section['name']}/", - 'description': f"{section['title']} documentation" - } - for section in devsite_structure['sections'] - ] + 'subsections': categories } }) @@ -153,11 +174,94 @@ def _generate_main_index(self, content_dir: Path, devsite_structure: Dict) -> No with open(index_file, 'w', encoding='utf-8') as f: f.write(index_content) + # Generate category index files + self._generate_category_indices(content_dir, devsite_structure) + logger.debug(f"Generated main index: {index_file}") + def _generate_category_indices(self, content_dir: Path, devsite_structure: Dict) -> None: + """Generate _index.md files for the 4 main categories""" + categories = { + 'tutorials': { + 'title': 'Tutorials', + 'description': TUTORIALS_DESCRIPTION, + 'weight': 10, + 'sections': [] + }, + 'how-to-guides': { + 'title': 'How-To Guides', + 'description': HOW_TO_GUIDES_DESCRIPTION, + 'weight': 20, + 'sections': [] + }, + 'explanations': { + 'title': 'Explanations', + 'description': EXPLANATIONS_DESCRIPTION, + 'weight': 30, + 'sections': [] + }, + 'reference': { + 'title': 'Reference', + 'description': REFERENCE_DESCRIPTION, + 'weight': 40, + 'sections': [] + } + } + + # Group sections by category + for section in devsite_structure['sections']: + section_name = section['name'] + if section_name in self.config['content_mapping']: + mapping = self.config['content_mapping'][section_name] + category_type = mapping['type'] + if category_type in categories: + categories[category_type]['sections'].append(section) + + # Generate index file for each category + for category_type, category_info in categories.items(): + category_dir = content_dir / category_type + category_dir.mkdir(parents=True, exist_ok=True) + + # Prepare subsections + subsections = [] + for section in category_info['sections']: + subsections.append({ + 'title': section['title'], + 'path': f"/{category_type}/{section['name']}/", + 'description': f"{section['title']} documentation" + }) + + # Render category index + template = self.template_env.get_template('section_index.jinja2') + index_content = template.render({ + 'section': { + 'title': category_info['title'], + 'description': category_info['description'], + 'type': 'docs', + 'weight': category_info['weight'], + 'subsections': subsections + } + }) + + # Write category index file + index_file = category_dir / '_index.md' + with open(index_file, 'w', encoding='utf-8') as f: + f.write(index_content) + + logger.debug(f"Generated category index: {index_file}") + def _generate_section_index(self, content_dir: Path, section: Dict) -> None: """Generate _index.md file for a section""" - section_dir = content_dir / section['name'] + # Determine the category for this section + section_name = section['name'] + category_type = 'docs' # default + + if section_name in self.config['content_mapping']: + mapping = self.config['content_mapping'][section_name] + category_type = mapping['type'] + + # Create section directory under its category + section_dir = content_dir / category_type / section['name'] section_dir.mkdir(parents=True, exist_ok=True) # Prepare subsections list @@ -284,8 +388,6 @@ def _generate_base_layout(self, layouts_dir: Path) -> None: {{ .Title }} | {{ .Site.Title }} - - {{ $style := resources.Get "scss/main.scss" | resources.ToCSS | resources.Minify }} @@ -412,127 +514,6 @@ def _generate_shortcodes(self, layouts_dir: Path) -> None: with open(toc_file, 'w', encoding='utf-8') as f: f.write(toc_content) - def _generate_main_scss(self, output_dir: Path) -> None: - """Generate main SCSS file that imports Bazel styles""" - scss_dir = output_dir / 'assets' / 'scss' - scss_dir.mkdir(parents=True, exist_ok=True) - - # Create main.scss that imports the Bazel styles - main_scss_content = '''// Main SCSS file for Hugo site -// Imports Bazel-specific styles - -@import "bazel"; - -// Additional site-wide styles -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - line-height: 1.6; - margin: 0; - padding: 0; -} - -.content { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} - -header { - background-color: $color-2; - color: $color-5; - padding: 1rem 0; -} - -.header-content { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; -} - -.logo { - color: $color-5; - text-decoration: none; - font-size: 1.5rem; - font-weight: bold; -} - -nav { - background-color: $color-3; - padding: 0.5rem 0; -} - -nav a { - color: $color-5; - text-decoration: none; - margin: 0 1rem; - padding: 0.5rem 0; -} - -nav a:hover { - text-decoration: underline; -} - -footer { - background-color: $color-4; - padding: 2rem 0; - margin-top: 3rem; - text-align: center; -} - -.section-list { - list-style: none; - padding: 0; -} - -.section-list li { - margin: 1rem 0; - padding: 1rem; - border: 1px solid #e1e4e8; - border-radius: 6px; -} - -.section-list a { - color: $color-2; - text-decoration: none; - font-weight: bold; -} - -.section-list a:hover { - text-decoration: underline; -} - -.section-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin: 2rem 0; -} - -.section-card { - padding: 1.5rem; - border: 1px solid #e1e4e8; - border-radius: 6px; - background: $color-5; -} - -.section-card h3 { - margin-top: 0; -} - -.section-card a { - color: $color-2; - text-decoration: none; -} - -.section-card a:hover { - text-decoration: underline; -} -''' - - main_scss_file = scss_dir / 'main.scss' - with open(main_scss_file, 'w', encoding='utf-8') as f: - f.write(main_scss_content) - def generate_menu_configuration(self, devsite_structure: Dict) -> Dict: """Generate Hugo menu configuration from Devsite structure""" menu_config = { @@ -549,22 +530,3 @@ def generate_menu_configuration(self, devsite_structure: Dict) -> Dict: menu_config['main'].append(menu_item) return menu_config - - def generate_params_configuration(self, devsite_structure: Dict) -> Dict: - """Generate Hugo params configuration for Docsy theme""" - params = { - 'github_repo': f"https://github.com/{self.config['source_repo']['owner']}/{self.config['source_repo']['name']}", - 'github_branch': self.config['source_repo']['branch'], - 'github_subdir': self.config['source_repo']['path'], - 'edit_page': True, - 'search': {'enabled': True}, - 'navigation': {'depth': 4}, - 'footer': {'enable': True}, - 'taxonomy': { - 'taxonomyCloud': ['tags', 'categories'], - 'taxonomyCloudTitle': ['Tag Cloud', 'Categories'], - 'taxonomyPageHeader': ['tags', 'categories'] - } - } - - return params