#!/usr/bin/env bash ############################################################## # Rust Documentation Generator with Custom Themes # Generates beautiful Rust documentation with custom color schemes ############################################################## set -euo pipefail # Color definitions using tput RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET="" if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then COLORS=$(tput colors 2>/dev/null || echo 0) if [[ ${COLORS:-0} -ge 8 ]]; then RED=$(tput setaf 1 2>/dev/null || echo "") GREEN=$(tput setaf 2 2>/dev/null || echo "") YELLOW=$(tput setaf 3 2>/dev/null || echo "") BLUE=$(tput setaf 4 2>/dev/null || echo "") MAGENTA=$(tput setaf 5 2>/dev/null || echo "") CYAN=$(tput setaf 6 2>/dev/null || echo "") BOLD=$(tput bold 2>/dev/null || echo "") DIM=$(tput dim 2>/dev/null || echo "") RESET=$(tput sgr0 2>/dev/null || echo "") fi fi # Script paths readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly ASSETS_DIR="${SCRIPT_DIR}/assets/doc_rust_generate" readonly THEME_DIR="${ASSETS_DIR}/theme" readonly TMP_DIR="${SCRIPT_DIR}/tmp" # Default values OUTPUT_DIR="${PWD}/output" PRIMARY_COLOR="#ff69b4" # Default pink STYLE="slate" FONT_SANS="Inter" FONT_MONO="JetBrains Mono" INPUT_FILES=() VERBOSE=false DRY_RUN=false SERVE=false OPEN=false SERVE_PORT=8000 # Style color mappings (neutral background colors) declare -A STYLE_COLORS=( ["slate"]="#64748b" ["zinc"]="#71717a" ["neutral"]="#737373" ["stone"]="#78716c" ["gray"]="#6b7280" ) ############################################################## # UI Functions ############################################################## info() { echo "${BLUE}${BOLD}==>${RESET} $*" } success() { echo "${GREEN}${BOLD}[OK]${RESET} $*" } error() { echo "${RED}${BOLD}[ERROR]${RESET} $*" >&2 } warning() { echo "${YELLOW}${BOLD}[WARN]${RESET} $*" } verbose() { if [[ "$VERBOSE" == true ]]; then echo "${DIM} -> $*${RESET}" fi return 0 } # Spinner for long-running operations spinner() { local pid=$1 local message=$2 local delay=0.1 local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' # Only show spinner if not verbose and output is a terminal if [[ "$VERBOSE" == true ]] || [[ ! -t 1 ]]; then wait "$pid" return $? fi echo -n "${message} " while kill -0 "$pid" 2>/dev/null; do local temp=${spinstr#?} printf "${CYAN}%c${RESET} " "$spinstr" spinstr=$temp${spinstr%"$temp"} sleep $delay printf "\b\b\b" done wait "$pid" local exit_code=$? if [[ $exit_code -eq 0 ]]; then printf "${GREEN}✓${RESET}\n" else printf "${RED}✗${RESET}\n" fi return $exit_code } ############################################################## # Help and Usage ############################################################## show_help() { cat << EOF ${BOLD}${MAGENTA}Rust Documentation Generator with Custom Themes${RESET} ${BOLD}USAGE:${RESET} $(basename "$0") [OPTIONS] ${BOLD}DESCRIPTION:${RESET} Generate beautiful Rust documentation with custom color schemes and styling. Supports various input types including Rust projects, individual files, and more. ${BOLD}ARGUMENTS:${RESET} Input file(s) or pattern(s) to document: - Rust project directories (containing Cargo.toml) - Individual .rs files - Markdown files (.md) - JSON/TOML configuration files - Glob patterns (e.g., src/**/*.rs) ${BOLD}OPTIONS:${RESET} -o, --output DIR Output directory for generated docs (default: \$PWD/output) -c, --color COLOR Primary accent color (hex format) (default: #ff69b4) Examples: #3498db, #10b981, #8b5cf6 -s, --style STYLE Background style theme Options: slate, zinc, neutral, stone, gray (default: slate) --font-sans FONT Google Font for body text (default: Inter) --font-mono FONT Google Font for code blocks (default: JetBrains Mono) --serve Start HTTP server after generation --open Open documentation in browser (implies --serve) -p, --port PORT Port for HTTP server (default: 8000) -v, --verbose Enable verbose output -d, --dry-run Show what would be done without executing -h, --help Show this help message ${BOLD}EXAMPLES:${RESET} ${DIM}# Generate docs for current Rust project${RESET} $(basename "$0") . ${DIM}# Custom color scheme${RESET} $(basename "$0") . -c "#3498db" -s zinc -o ./docs ${DIM}# Document specific files${RESET} $(basename "$0") src/lib.rs src/main.rs -o ./api-docs ${DIM}# Use custom fonts${RESET} $(basename "$0") . --font-sans "Roboto" --font-mono "Fira Code" ${DIM}# Generate and open in browser${RESET} $(basename "$0") . --open ${BOLD}NOTES:${RESET} - Requires: cargo, rustdoc, bc, yq, jq, python3 with jinja2 - Colors are automatically generated in light and dark variants - Google Fonts are automatically imported - Mermaid.js diagrams are automatically rendered EOF } ############################################################## # Dependency Checks ############################################################## check_dependencies() { local missing=() command -v cargo >/dev/null 2>&1 || missing+=("cargo") command -v rustdoc >/dev/null 2>&1 || missing+=("rustdoc") command -v bc >/dev/null 2>&1 || missing+=("bc") command -v yq >/dev/null 2>&1 || missing+=("yq") command -v jq >/dev/null 2>&1 || missing+=("jq") command -v python3 >/dev/null 2>&1 || missing+=("python3") if ! python3 -c "import jinja2" 2>/dev/null; then missing+=("python3-jinja2") fi if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required dependencies: ${missing[*]}" exit 1 fi # Check for helper scripts if [[ ! -x "${SCRIPT_DIR}/css_color_palette.sh" ]]; then error "Helper script not found: ${SCRIPT_DIR}/css_color_palette.sh" exit 1 fi if [[ ! -x "${SCRIPT_DIR}/jinja_template_render.sh" ]]; then error "Helper script not found: ${SCRIPT_DIR}/jinja_template_render.sh" exit 1 fi return 0 } ############################################################## # Input Processing ############################################################## validate_color() { local color="$1" # Remove leading # if present color="${color#\#}" if [[ ! "$color" =~ ^[0-9A-Fa-f]{3}$ ]] && [[ ! "$color" =~ ^[0-9A-Fa-f]{6}$ ]]; then error "Invalid hex color: $1" error "Expected format: #RGB or #RRGGBB" exit 1 fi # Ensure 6-digit format if [[ ${#color} -eq 3 ]]; then color="${color:0:1}${color:0:1}${color:1:1}${color:1:1}${color:2:1}${color:2:1}" fi echo "#${color}" } validate_style() { local style="$1" case "$style" in slate|zinc|neutral|stone|gray) echo "$style" ;; *) error "Invalid style: $style" error "Valid options: slate, zinc, neutral, stone, gray" exit 1 ;; esac } ############################################################## # Color Palette Generation ############################################################## generate_color_palette() { local primary_color="$1" local style="$2" local output_file="$3" verbose "Primary color: $primary_color" verbose "Style: $style" # Generate primary color palette (monochromatic) if [[ "$VERBOSE" == false ]]; then info "Generating color palette..." fi if ! "${SCRIPT_DIR}/css_color_palette.sh" "$primary_color" \ -p monochromatic \ -o "${TMP_DIR}/primary_palette.yaml" \ -m dark \ -s all >/dev/null 2>&1; then error "Failed to generate primary color palette" return 1 fi verbose "Generated primary palette: ${TMP_DIR}/primary_palette.yaml" if [[ "$VERBOSE" == false ]]; then success "Color palette generated" fi } ############################################################## # CSS Variables Generation ############################################################## generate_css_variables() { local primary_color="$1" local style="$2" local output_file="$3" info "Generating CSS variables..." # Read the generated palette local palette_file="${TMP_DIR}/primary_palette.yaml" if [[ ! -f "$palette_file" ]]; then error "Palette file not found: $palette_file" return 1 fi # Extract color values from the palette local primary_50 primary_100 primary_200 primary_300 primary_400 local primary_500 primary_600 primary_700 primary_800 primary_900 primary_950 primary_50=$(yq -r '.colors.primary."50"' "$palette_file") primary_100=$(yq -r '.colors.primary."100"' "$palette_file") primary_200=$(yq -r '.colors.primary."200"' "$palette_file") primary_300=$(yq -r '.colors.primary."300"' "$palette_file") primary_400=$(yq -r '.colors.primary."400"' "$palette_file") primary_500=$(yq -r '.colors.primary."500"' "$palette_file") primary_600=$(yq -r '.colors.primary."600"' "$palette_file") primary_700=$(yq -r '.colors.primary."700"' "$palette_file") primary_800=$(yq -r '.colors.primary."800"' "$palette_file") primary_900=$(yq -r '.colors.primary."900"' "$palette_file") primary_950=$(yq -r '.colors.primary."950"' "$palette_file") # Get style-specific colors (from existing theme.yaml) local style_file="${THEME_DIR}/theme.yaml" # Generate CSS custom properties cat > "$output_file" << EOF /* Generated CSS Variables for Rustdoc Theme */ /* Primary Color: ${primary_color} */ /* Style: ${style} */ /* Import Google Fonts */ @import url('https://fonts.googleapis.com/css2?family=${FONT_SANS// /+}:wght@300;400;500;600;700&family=${FONT_MONO// /+}:wght@400;500;600;700&display=swap'); /* Base variables for all themes */ :root { /* Primary color palette */ --primary-50: ${primary_50}; --primary-100: ${primary_100}; --primary-200: ${primary_200}; --primary-300: ${primary_300}; --primary-400: ${primary_400}; --primary-500: ${primary_500}; --primary-600: ${primary_600}; --primary-700: ${primary_700}; --primary-800: ${primary_800}; --primary-900: ${primary_900}; --primary-950: ${primary_950}; /* Google Fonts */ --font-family: '${FONT_SANS}', 'Fira Sans', Arial, sans-serif; --font-family-code: '${FONT_MONO}', 'Fira Mono', monospace; } /* Dark theme overrides */ :root[data-theme="dark"] { EOF # Now read the theme.yaml and replace color references with our palette verbose "Processing theme variables..." # Read theme.yaml and substitute colors while IFS=': ' read -r key value; do # Skip empty lines and comments [[ -z "$key" || "$key" =~ ^# ]] && continue # Remove quotes and leading/trailing whitespace value="${value#\"}" value="${value%\"}" value="${value#\'}" value="${value%\'}" value="${value# }" value="${value% }" # Replace hardcoded colors with our palette where appropriate # This is a simple substitution - for primary/accent colors use our palette if [[ "$value" == "#ff69b4" || "$value" == "#ff1493" || "$value" == "#ec407a" ]]; then value="${primary_500}" elif [[ "$value" == "#f06292" ]]; then value="${primary_400}" fi echo " --${key}: ${value};" >> "$output_file" done < <(grep -E '^[a-zA-Z]' "$style_file" || true) # Close the dark theme block echo "}" >> "$output_file" # Add light theme variant (with adjusted colors) cat >> "$output_file" << EOF /* Light theme overrides */ :root[data-theme="light"] { EOF # For light theme, read theme.yaml again but use lighter colors while IFS=': ' read -r key value; do # Skip empty lines and comments [[ -z "$key" || "$key" =~ ^# ]] && continue # Remove quotes and leading/trailing whitespace value="${value#\"}" value="${value%\"}" value="${value#\'}" value="${value%\'}" value="${value# }" value="${value% }" # For light theme, invert dark backgrounds to light case "$key" in *background*|*-bg-*) # Use lighter shades for backgrounds if [[ "$value" == "#2f3542" ]]; then value="#f8f9fa" # main background elif [[ "$value" == "#1e272e" ]]; then value="#ffffff" # secondary background elif [[ "$value" == "#24272e" ]]; then value="#f1f3f5" # hover background fi ;; *color*|*-fg-*) # Use darker text for light backgrounds if [[ "$value" == "#e8eaed" ]]; then value="#2f3542" # main text color elif [[ "$value" == "#ffffff" ]]; then value="#000000" # white text -> black fi ;; *border*) # Adjust borders for light theme if [[ "$value" == "#57606f" ]]; then value="#dee2e6" fi ;; esac # Replace primary accent colors with our palette if [[ "$value" == "#ff69b4" || "$value" == "#ff1493" || "$value" == "#ec407a" ]]; then value="${primary_600}" # Slightly darker for light theme elif [[ "$value" == "#f06292" ]]; then value="${primary_500}" fi echo " --${key}: ${value};" >> "$output_file" done < <(grep -E '^[a-zA-Z]' "$style_file" || true) # Close the light theme block echo "}" >> "$output_file" verbose "Generated CSS variables: $output_file" success "CSS variables generated" } ############################################################## # Render Header Template ############################################################## render_header_template() { local output_file="$1" info "Rendering header template..." # Process mermaid.yaml variables to match our theme local mermaid_yaml="${TMP_DIR}/mermaid_processed.yaml" # Copy and update mermaid colors cp "${THEME_DIR}/mermaid.yaml" "$mermaid_yaml" # Update with our primary color yq -i ".primaryColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".lineColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".border1 = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".noteBorderColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".arrowheadColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".clusterBorder = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".defaultLinkColor = \"${PRIMARY_COLOR}\"" "$mermaid_yaml" yq -i ".fontFamily = \"${FONT_SANS}, system-ui, -apple-system, sans-serif\"" "$mermaid_yaml" verbose "Processed mermaid config: $mermaid_yaml" # Render the Jinja2 template if ! "${SCRIPT_DIR}/jinja_template_render.sh" \ -f "$mermaid_yaml" \ -o "${TMP_DIR}" \ "${THEME_DIR}/header.html.jinja"; then error "Failed to render header template" return 1 fi # Move rendered file to expected location if different if [[ "${TMP_DIR}/header.html" != "$output_file" ]]; then mv "${TMP_DIR}/header.html" "$output_file" fi verbose "Rendered header: $output_file" success "Header template rendered" } ############################################################## # Consolidate CSS ############################################################## consolidate_css() { local output_file="$1" info "Consolidating CSS files..." # Combine variables.css and theme.css cat "${TMP_DIR}/variables.css" > "$output_file" echo "" >> "$output_file" cat "${THEME_DIR}/theme.css" >> "$output_file" verbose "Consolidated CSS: $output_file" success "CSS consolidated" } ############################################################## # Run Rustdoc ############################################################## run_rustdoc() { local inputs=("$@") info "Running rustdoc..." local header_file="${TMP_DIR}/header.html" local css_file="${TMP_DIR}/theme-combined.css" # Determine if we're documenting a cargo project or individual files local is_cargo_project=false for input in "${inputs[@]}"; do if [[ -f "${input}/Cargo.toml" ]] || [[ -f "${input%/}/Cargo.toml" ]]; then is_cargo_project=true break fi done if [[ "$is_cargo_project" == true ]]; then info "Detected Cargo project, using 'cargo doc'..." # Find the Cargo.toml directory local cargo_dir="" for input in "${inputs[@]}"; do if [[ -f "${input}/Cargo.toml" ]]; then cargo_dir="$input" break elif [[ -f "${input%/}/Cargo.toml" ]]; then cargo_dir="${input%/}" break fi done # Build cargo doc command with custom theme local cargo_args=( "doc" "--no-deps" "--target-dir" "${OUTPUT_DIR}" ) # Set RUSTDOCFLAGS environment variable for custom styling export RUSTDOCFLAGS="--html-in-header ${header_file} --extend-css ${css_file}" if [[ "$VERBOSE" == true ]]; then cargo_args+=("-v") fi verbose "Running: cargo ${cargo_args[*]}" verbose "RUSTDOCFLAGS: $RUSTDOCFLAGS" if [[ "$DRY_RUN" == true ]]; then warning "Dry run: would execute cargo ${cargo_args[*]}" else if [[ "$VERBOSE" == false ]]; then info "Running cargo doc (this may take a while)..." fi # In non-verbose mode, show cargo progress messages if [[ "$VERBOSE" == true ]]; then # Verbose: show everything if ! (cd "$cargo_dir" && cargo "${cargo_args[@]}"); then error "cargo doc failed" return 1 fi else # Non-verbose: filter output to show only important messages if ! (cd "$cargo_dir" && cargo "${cargo_args[@]}" 2>&1 | grep -E '(Documenting|Compiling|Finished|error|warning)' || true); then # Check if cargo actually failed if ! (cd "$cargo_dir" && cargo "${cargo_args[@]}" >/dev/null 2>&1); then error "cargo doc failed" return 1 fi fi fi success "Documentation generated successfully" info "Output directory: ${OUTPUT_DIR}/doc" fi else # Document individual files using rustdoc directly warning "Individual file documentation not fully implemented yet" warning "Please provide a Cargo project directory" return 1 fi } ############################################################## # Serve and Open Documentation ############################################################## serve_and_open_docs() { local doc_dir="${OUTPUT_DIR}/doc" local index_file="${doc_dir}/universal_lsp/index.html" # Find the actual index.html file (might be in different location) if [[ ! -f "$index_file" ]]; then # Try to find any index.html in the doc directory index_file=$(find "$doc_dir" -name "index.html" -type f | head -1) if [[ -z "$index_file" ]]; then warning "Could not find index.html in documentation" index_file="${doc_dir}/index.html" fi fi # Get the relative path from doc_dir to index_file for the URL local rel_path="${index_file#$doc_dir/}" local url="http://localhost:${SERVE_PORT}/${rel_path}" info "Starting HTTP server on port ${SERVE_PORT}..." echo "" # Start Python HTTP server in the background cd "$doc_dir" || { error "Failed to change to documentation directory: $doc_dir" return 1 } # Kill any existing server on this port if lsof -Pi ":${SERVE_PORT}" -sTCP:LISTEN -t >/dev/null 2>&1; then warning "Port ${SERVE_PORT} is already in use. Trying to kill existing process..." lsof -Pi ":${SERVE_PORT}" -sTCP:LISTEN -t | xargs kill -9 2>/dev/null || true sleep 1 fi # Start server python3 -m http.server "$SERVE_PORT" >/dev/null 2>&1 & local server_pid=$! # Give the server a moment to start sleep 1 # Check if server started successfully if ! kill -0 "$server_pid" 2>/dev/null; then error "Failed to start HTTP server" return 1 fi success "Server started (PID: $server_pid)" info "Documentation available at: ${CYAN}${url}${RESET}" # Open in browser if requested if [[ "$OPEN" == true ]]; then info "Opening documentation in browser..." # Try different browser openers if command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 & elif command -v open >/dev/null 2>&1; then open "$url" >/dev/null 2>&1 & elif command -v sensible-browser >/dev/null 2>&1; then sensible-browser "$url" >/dev/null 2>&1 & else warning "Could not detect browser opener (xdg-open, open, sensible-browser)" info "Please open manually: $url" fi sleep 1 success "Browser opened" fi echo "" echo "${BOLD}${GREEN}Server running!${RESET}" echo "${DIM}Press Ctrl+C to stop the server${RESET}" echo "" # Wait for Ctrl+C trap "echo ''; info 'Shutting down server...'; kill $server_pid 2>/dev/null; success 'Server stopped'; exit 0" INT TERM # Keep the script running wait "$server_pid" } ############################################################## # Cleanup ############################################################## cleanup() { if [[ "$VERBOSE" == true ]]; then verbose "Temporary files preserved in: ${TMP_DIR}" fi } trap cleanup EXIT ############################################################## # Main Function ############################################################## main() { # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -o|--output) OUTPUT_DIR="$2" shift 2 ;; -c|--color) PRIMARY_COLOR=$(validate_color "$2") shift 2 ;; -s|--style) STYLE=$(validate_style "$2") shift 2 ;; --font-sans) FONT_SANS="$2" shift 2 ;; --font-mono) FONT_MONO="$2" shift 2 ;; --serve) SERVE=true shift ;; --open) OPEN=true SERVE=true # Opening implies serving shift ;; -p|--port) SERVE_PORT="$2" shift 2 ;; -v|--verbose) VERBOSE=true shift ;; -d|--dry-run) DRY_RUN=true shift ;; -h|--help) show_help exit 0 ;; -*) error "Unknown option: $1" echo "Use -h or --help for usage information" exit 1 ;; *) INPUT_FILES+=("$1") shift ;; esac done # Print banner echo "${MAGENTA}${BOLD}" echo "==============================================" echo " Rust Documentation Generator" echo "==============================================" echo "${RESET}" # Check dependencies info "Checking dependencies..." check_dependencies success "All dependencies found" # Validate inputs if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then error "No input files specified" echo "Use -h or --help for usage information" exit 1 fi # Create temporary directory mkdir -p "$TMP_DIR" verbose "Temporary directory: $TMP_DIR" # Show configuration echo "" info "Configuration:" echo " Primary Color: ${PRIMARY_COLOR}" echo " Style: ${STYLE}" echo " Font Sans: ${FONT_SANS}" echo " Font Mono: ${FONT_MONO}" echo " Output: ${OUTPUT_DIR}" echo " Inputs: ${INPUT_FILES[*]}" echo "" # Generate color palette generate_color_palette "$PRIMARY_COLOR" "$STYLE" "${TMP_DIR}/palette.yaml" # Generate CSS variables generate_css_variables "$PRIMARY_COLOR" "$STYLE" "${TMP_DIR}/variables.css" # Render header template render_header_template "${TMP_DIR}/header.html" # Consolidate CSS consolidate_css "${TMP_DIR}/theme-combined.css" # Run rustdoc run_rustdoc "${INPUT_FILES[@]}" echo "" success "Documentation generation complete!" if [[ "$DRY_RUN" == false ]]; then info "View your documentation:" echo " ${CYAN}${OUTPUT_DIR}/doc/index.html${RESET}" # Serve and/or open documentation if requested if [[ "$SERVE" == true ]]; then echo "" serve_and_open_docs fi fi echo "" } # Run main function main "$@"