383 lines
10 KiB
Bash
Executable File
383 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# StarForth — Steady-State Virtual Machine Runtime
|
||
#
|
||
# Copyright (c) 2023–2025 Robert A. James
|
||
# All rights reserved.
|
||
#
|
||
# This file is part of the StarForth project.
|
||
#
|
||
# Licensed under the StarForth License, Version 1.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
#
|
||
# You may obtain a copy of the License at:
|
||
# https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
#
|
||
# This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
# express or implied, including but not limited to the warranties of
|
||
# merchantability, fitness for a particular purpose, and noninfringement.
|
||
#
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
#
|
||
# StarForth — Steady-State Virtual Machine Runtime
|
||
# Copyright (c) 2023–2025 Robert A. James
|
||
# All rights reserved.
|
||
#
|
||
# This file is part of the StarForth project.
|
||
#
|
||
# Licensed under the StarForth License, Version 1.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
#
|
||
# You may obtain a copy of the License at:
|
||
# https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
#
|
||
# This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
# express or implied, including but not limited to the warranties of
|
||
# merchantability, fitness for a particular purpose, and noninfringement.
|
||
#
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
#
|
||
#
|
||
|
||
################################################################################
|
||
# StarForth Markdown to AsciiDoc Converter
|
||
#
|
||
# Automatically converts markdown files to AsciiDoc and intelligently
|
||
# categorizes them using Claude AI
|
||
#
|
||
# Features:
|
||
# - Scans repo for markdown files (excluding specified ones)
|
||
# - Converts .md to .adoc using pandoc
|
||
# - Uses Claude to determine best category
|
||
# - Moves files to appropriate docs subdirectory
|
||
# - Adds TOC attributes and back-links
|
||
# - Stages changes and deletes originals
|
||
#
|
||
# Usage: ./scripts/convert-markdown-to-asciidoc.sh
|
||
#
|
||
# Environment:
|
||
# - CLAUDE_API_KEY: Optional, for intelligent categorization
|
||
# - PRE_COMMIT_DRY_RUN: Set to 1 for preview mode
|
||
################################################################################
|
||
|
||
set -e
|
||
|
||
# Color codes
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Configuration
|
||
EXCLUDED_FILES=(
|
||
"README.md"
|
||
"CONTRIBUTE.md"
|
||
"SECURITY.md"
|
||
"CODE_OF_CONDUCT.md"
|
||
"NOTICE.md"
|
||
)
|
||
|
||
DOCS_DIR="docs/src"
|
||
CATEGORIES=(
|
||
"getting-started"
|
||
"build-and-tooling"
|
||
"platform-integration"
|
||
"architecture-internals"
|
||
"performance-profiling"
|
||
"testing-quality"
|
||
"development"
|
||
"governance"
|
||
"internal"
|
||
)
|
||
|
||
DRY_RUN=${PRE_COMMIT_DRY_RUN:-0}
|
||
|
||
# Logging functions
|
||
log_info() {
|
||
echo -e "${BLUE}ℹ${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}✓${NC} $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}✗${NC} $1" >&2
|
||
}
|
||
|
||
# Check prerequisites
|
||
check_prerequisites() {
|
||
if ! command -v pandoc &> /dev/null; then
|
||
log_error "pandoc not found. Install with: sudo apt-get install pandoc"
|
||
return 1
|
||
fi
|
||
|
||
if [ -z "$CLAUDE_API_KEY" ]; then
|
||
log_warn "CLAUDE_API_KEY not set. Skipping intelligent categorization."
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# Check if file is in exclude list
|
||
is_excluded() {
|
||
local file=$1
|
||
local basename=$(basename "$file")
|
||
|
||
# Exclude all dot files and directories
|
||
if [[ "$basename" == .* ]]; then
|
||
return 0
|
||
fi
|
||
|
||
for excluded in "${EXCLUDED_FILES[@]}"; do
|
||
if [[ "$basename" == "$excluded" ]]; then
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
if [[ "$file" == *"StarForth-Governance"* ]]; then
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# Convert markdown to AsciiDoc
|
||
convert_to_asciidoc() {
|
||
local md_file=$1
|
||
local adoc_file="${md_file%.md}.adoc"
|
||
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
log_info "[DRY RUN] Would convert: $md_file → $adoc_file"
|
||
return 0
|
||
fi
|
||
|
||
pandoc -f markdown -t asciidoc "$md_file" -o "$adoc_file"
|
||
return $?
|
||
}
|
||
|
||
# Use Claude to categorize document
|
||
categorize_document() {
|
||
local adoc_file=$1
|
||
local filename=$(basename "$adoc_file")
|
||
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
log_info "[DRY RUN] Would send to Claude for categorization: $filename"
|
||
echo "docs" # Default category for dry-run
|
||
return 0
|
||
fi
|
||
|
||
local content=$(head -c 2000 "$adoc_file") # First 2000 chars for context
|
||
|
||
local response=$(curl -s -X POST "https://api.anthropic.com/v1/messages" \
|
||
-H "Content-Type: application/json" \
|
||
-H "x-api-key: $CLAUDE_API_KEY" \
|
||
-H "anthropic-version: 2023-06-01" \
|
||
-d "{
|
||
\"model\": \"claude-haiku-4-5-20251001\",
|
||
\"max_tokens\": 50,
|
||
\"system\": \"You are a documentation categorization expert. Given a document excerpt, respond with ONLY the category name (no explanation). Choose from: getting-started, build-and-tooling, platform-integration, architecture-internals, performance-profiling, testing-quality, development, governance, internal. If unsure, respond with 'docs'.\",
|
||
\"messages\": [
|
||
{
|
||
\"role\": \"user\",
|
||
\"content\": \"Categorize this document based on its content. Filename: $filename\n\nContent excerpt:\n$content\"
|
||
}
|
||
]
|
||
}")
|
||
|
||
# Extract category from response
|
||
local category=$(echo "$response" | grep -o '"text":"[^"]*"' | sed 's/"text":"\([^"]*\)"/\1/' | tr -d ' ' | head -1)
|
||
|
||
# Validate category exists
|
||
local valid=0
|
||
for cat in "${CATEGORIES[@]}"; do
|
||
if [[ "$category" == "$cat" ]]; then
|
||
valid=1
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ $valid -eq 0 ]; then
|
||
category="docs"
|
||
fi
|
||
|
||
echo "$category"
|
||
}
|
||
|
||
# Add TOC attributes to AsciiDoc file
|
||
add_toc_attributes() {
|
||
local adoc_file=$1
|
||
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
log_info "[DRY RUN] Would add TOC attributes to: $adoc_file"
|
||
return 0
|
||
fi
|
||
|
||
# Get first line (title)
|
||
local first_line=$(head -n 1 "$adoc_file")
|
||
|
||
# Create temp file with title + TOC attributes + rest
|
||
{
|
||
echo "$first_line"
|
||
echo ":toc: left"
|
||
echo ":toc-title: Contents"
|
||
echo ":toclevels: 3"
|
||
echo ""
|
||
tail -n +2 "$adoc_file"
|
||
} > "$adoc_file.tmp"
|
||
|
||
mv "$adoc_file.tmp" "$adoc_file"
|
||
}
|
||
|
||
# Add back-to-README link
|
||
add_back_link() {
|
||
local adoc_file=$1
|
||
local relative_path=$2
|
||
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
log_info "[DRY RUN] Would add back-link to: $adoc_file"
|
||
return 0
|
||
fi
|
||
|
||
# Find line number after :toclevels:
|
||
local line_num=$(grep -n "^:toclevels:" "$adoc_file" | head -1 | cut -d: -f1)
|
||
|
||
if [ -n "$line_num" ]; then
|
||
sed -i "${line_num}a\\
|
||
xref:${relative_path}[← Back to Documentation Index]\n" "$adoc_file"
|
||
fi
|
||
}
|
||
|
||
# Move file to appropriate directory
|
||
move_to_category() {
|
||
local adoc_file=$1
|
||
local category=$2
|
||
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
if [ "$category" = "docs" ]; then
|
||
log_info "[DRY RUN] Would move: $adoc_file → $DOCS_DIR/"
|
||
else
|
||
log_info "[DRY RUN] Would move: $adoc_file → $DOCS_DIR/$category/"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
local basename=$(basename "$adoc_file")
|
||
local target
|
||
local rel_path
|
||
|
||
# If category is "docs" (fallback/default), put directly in docs/ root
|
||
if [ "$category" = "docs" ]; then
|
||
mkdir -p "$DOCS_DIR"
|
||
target="$DOCS_DIR/$basename"
|
||
rel_path="./README.adoc" # Same directory
|
||
else
|
||
# Otherwise create category subdirectory
|
||
mkdir -p "$DOCS_DIR/$category"
|
||
target="$DOCS_DIR/$category/$basename"
|
||
rel_path="../README.adoc" # One level up
|
||
fi
|
||
|
||
mv "$adoc_file" "$target"
|
||
add_back_link "$target" "$rel_path"
|
||
|
||
echo "$target"
|
||
}
|
||
|
||
# Main processing loop
|
||
main() {
|
||
log_info "Scanning for markdown files to convert..."
|
||
|
||
local api_available=0
|
||
check_prerequisites && api_available=1
|
||
|
||
if [ $api_available -eq 0 ]; then
|
||
log_warn "Claude API not available - will move files to ./docs/ as fallback"
|
||
echo ""
|
||
fi
|
||
|
||
local converted_count=0
|
||
local failed_count=0
|
||
|
||
# Find all markdown files
|
||
while IFS= read -r md_file; do
|
||
if is_excluded "$md_file"; then
|
||
log_info "Skipping excluded file: $md_file"
|
||
continue
|
||
fi
|
||
|
||
log_info "Processing: $md_file"
|
||
|
||
# Convert to AsciiDoc
|
||
if ! convert_to_asciidoc "$md_file"; then
|
||
log_error "Failed to convert: $md_file"
|
||
((failed_count++))
|
||
continue
|
||
fi
|
||
|
||
local adoc_file="${md_file%.md}.adoc"
|
||
|
||
# Add TOC attributes
|
||
add_toc_attributes "$adoc_file"
|
||
|
||
# Categorize using Claude (if available), otherwise use docs/ as safe default
|
||
local category="docs"
|
||
if [ $api_available -eq 1 ]; then
|
||
local claude_category=$(categorize_document "$adoc_file" 2>/dev/null)
|
||
if [ -n "$claude_category" ] && [ "$claude_category" != "docs" ]; then
|
||
category="$claude_category"
|
||
log_success "Categorized as: $category"
|
||
else
|
||
log_warn "Claude categorization failed or returned default, using: docs"
|
||
category="docs"
|
||
fi
|
||
else
|
||
log_warn "Claude API unavailable - using fallback category: docs"
|
||
category="docs"
|
||
fi
|
||
|
||
# Move to appropriate directory (ensures file is never lost)
|
||
local final_path=$(move_to_category "$adoc_file" "$category")
|
||
log_success "Moved to: $final_path"
|
||
|
||
# Stage the new AsciiDoc file (will be added if this is run from pre-commit)
|
||
if [ -n "$(git rev-parse --git-dir 2>/dev/null)" ]; then
|
||
git add "$final_path" 2>/dev/null || true
|
||
fi
|
||
|
||
# Delete original markdown
|
||
if [ "$DRY_RUN" != "1" ]; then
|
||
rm "$md_file"
|
||
log_success "Deleted: $md_file"
|
||
fi
|
||
|
||
((converted_count++))
|
||
|
||
done < <(find . -name "*.md" -type f ! -path "*/StarForth-Governance/*" ! -path "*/.git/*" 2>/dev/null)
|
||
|
||
# Summary
|
||
echo ""
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
log_info "DRY RUN: No changes were made"
|
||
fi
|
||
|
||
if [ $converted_count -gt 0 ]; then
|
||
log_success "Converted $converted_count markdown file(s) to AsciiDoc"
|
||
fi
|
||
|
||
if [ $failed_count -gt 0 ]; then
|
||
log_warn "Failed to convert $failed_count file(s) - they were NOT deleted"
|
||
fi
|
||
|
||
return $failed_count
|
||
}
|
||
|
||
main "$@" |