update gpt

This commit is contained in:
syui 2025-06-06 03:18:20 +09:00
parent a9dca2fe38
commit c0e4dc63ea
Signed by: syui
GPG Key ID: 5417CFEBAD92DF56
18 changed files with 2827 additions and 51 deletions

View File

@ -32,6 +32,12 @@ axum = "0.7"
tower = "0.5"
tower-http = { version = "0.5", features = ["cors", "fs"] }
hyper = { version = "1.0", features = ["full"] }
# Documentation generation dependencies
syn = { version = "2.0", features = ["full", "parsing", "visit"] }
quote = "1.0"
ignore = "0.4"
git2 = "0.18"
regex = "1.0"
[dev-dependencies]
tempfile = "3.14"

418
README.md
View File

@ -4,15 +4,40 @@ A Rust-based static blog generator with AI integration capabilities.
## Overview
ai.log is part of the ai ecosystem - a static site generator that creates blogs with built-in AI features for content enhancement and atproto integration.
ai.log is part of the ai ecosystem - a static site generator that creates blogs with built-in AI features for content enhancement and atproto integration. The system follows the yui system principles with dual-layer MCP architecture.
## Architecture
### Dual MCP Integration
**ai.log MCP Server (API Layer)**
- **Role**: Independent blog API
- **Port**: 8002
- **Location**: `./src/mcp/`
- **Function**: Core blog generation and management
**ai.gpt Integration (Server Layer)**
- **Role**: AI integration gateway
- **Port**: 8001 (within ai.gpt)
- **Location**: `../src/aigpt/mcp_server.py`
- **Function**: AI memory system + HTTP proxy to ai.log
### Data Flow
```
Claude Code → ai.gpt (Server/AI) → ai.log (API/Blog) → Static Site
↑ ↑
Memory System File Operations
Relationship AI Markdown Processing
Context Analysis Template Rendering
```
## Features
- Static blog generation (inspired by Zola)
- AI-powered article editing and enhancement
- Automatic translation (ja → en)
- AI comment system integrated with atproto
- OAuth authentication via atproto accounts
- **Static Blog Generation**: Inspired by Zola, built with Rust
- **AI-Powered Content**: Memory-driven article generation via ai.gpt
- **🌍 Ollama Translation**: Multi-language markdown translation with structure preservation
- **atproto Integration**: OAuth authentication and comment system (planned)
- **MCP Integration**: Seamless Claude Code workflow
## Installation
@ -22,6 +47,8 @@ cargo install ailog
## Usage
### Standalone Mode
```bash
# Initialize a new blog
ailog init myblog
@ -35,53 +62,380 @@ ailog build
# Serve locally
ailog serve
# Start MCP server
ailog mcp --port 8002
# Generate documentation
ailog doc readme --with-ai
ailog doc api --output ./docs
ailog doc structure --include-deps
# Translate documents (requires Ollama)
ailog doc translate --input README.md --target-lang en
ailog doc translate --input docs/api.md --target-lang ja --model qwen2.5:latest
# Clean build files
ailog clean
```
### AI Ecosystem Integration
When integrated with ai.gpt, use natural language:
- "ブログ記事を書いて" → Triggers `log_ai_content`
- "記事一覧を見せて" → Triggers `log_list_posts`
- "ブログをビルドして" → Triggers `log_build_blog`
### Documentation & Translation
Generate comprehensive documentation and translate content:
- "READMEを生成して" → Triggers `log_generate_docs`
- "APIドキュメントを作成して" → Generates API documentation
- "プロジェクト構造を解析して" → Creates structure documentation
- "このファイルを英語に翻訳して" → Triggers `log_translate_document`
- "マークダウンを日本語に変換して" → Uses Ollama for translation
## MCP Tools
### ai.log Server (Port 8002)
- `create_blog_post` - Create new blog post
- `list_blog_posts` - List existing posts
- `build_blog` - Build static site
- `get_post_content` - Get post by slug
- `translate_document` ⭐ - Ollama-powered markdown translation
- `generate_documentation` ⭐ - Code analysis and documentation generation
### ai.gpt Integration (Port 8001)
- `log_create_post` - Proxy to ai.log + error handling
- `log_list_posts` - Proxy to ai.log + formatting
- `log_build_blog` - Proxy to ai.log + AI features
- `log_get_post` - Proxy to ai.log + context
- `log_system_status` - Health check for ai.log
- `log_ai_content` ⭐ - AI memory → blog content generation
- `log_translate_document` 🌍 - Document translation via Ollama
- `log_generate_docs` 📚 - Documentation generation
### Documentation Generation Tools
- `doc readme` - Generate README.md from project analysis
- `doc api` - Generate API documentation
- `doc structure` - Analyze and document project structure
- `doc changelog` - Generate changelog from git history
- `doc translate` 🌍 - Multi-language document translation
### Translation Features
- **Language Support**: English, Japanese, Chinese, Korean, Spanish
- **Markdown Preservation**: Code blocks, links, images, tables maintained
- **Auto-Detection**: Automatically detects Japanese content
- **Ollama Integration**: Uses local AI models for privacy and cost-efficiency
- **Smart Processing**: Section-by-section translation with structure awareness
## Configuration
Configuration files are stored in `~/.config/syui/ai/log/`
### ai.log Configuration
- Location: `~/.config/syui/ai/log/`
- Format: TOML configuration
## AI Integration (Planned)
### ai.gpt Integration
- Configuration: `../config.json`
- Auto-detection: ai.log tools enabled when `./log/` directory exists
- System prompt: Automatically triggers blog tools for related queries
- Automatic content suggestions and corrections
- Multi-language support with AI translation
- AI-generated comments linked to atproto accounts
## AI Integration Features
### Memory-Driven Content Generation
- **Source**: ai.gpt memory system
- **Process**: Contextual memories → AI analysis → Blog content
- **Output**: Structured markdown with personal insights
### Automatic Workflows
- Daily blog posts from accumulated memories
- Content enhancement and suggestions
- Related article recommendations
- Multi-language content generation
## atproto Integration (Planned)
Implements OAuth 2.0 for user authentication:
- Users can comment using their atproto accounts
- Comments are stored in atproto collections
- Full data sovereignty for users
### OAuth 2.0 Authentication
- Client metadata: `public/client-metadata.json`
- Comment system integration
- Data sovereignty: Users own their comments
- Collection storage in atproto
### Comment System
- atproto account login
- Distributed comment storage
- Real-time comment synchronization
## Build & Deploy
Designed for GitHub Actions and Cloudflare Pages deployment. Push to main branch triggers automatic build and deploy.
### GitHub Actions
```yaml
# .github/workflows/gh-pages.yml
- name: Build ai.log
run: |
cd log
cargo build --release
./target/release/ailog build
```
### Cloudflare Pages
- Static output: `./public/`
- Automatic deployment on main branch push
- AI content generation during build process
## Development Status
Currently implemented:
- ✅ Project structure and Cargo.toml setup
- ✅ Basic command-line interface (init, new, build, serve, clean)
- ✅ Configuration system with TOML support
- ✅ Markdown parsing with frontmatter support
- ✅ Template system with Handlebars
- ✅ Static site generation with posts and pages
- ✅ Development server with hot reload
- ✅ AI integration foundation (GPT client, translator, comment system)
- ✅ atproto client with OAuth support
- ✅ MCP server integration for AI tools
- ✅ Test blog with sample content and styling
### ✅ Completed Features
- Project structure and Cargo.toml setup
- CLI interface (init, new, build, serve, clean, mcp, doc)
- Configuration system with TOML support
- Markdown parsing with frontmatter support
- Template system with Handlebars
- Static site generation with posts and pages
- Development server with hot reload
- **MCP server integration (both layers)**
- **ai.gpt integration with 6 tools**
- **AI memory system connection**
- **📚 Documentation generation from code**
- **🔍 Rust project analysis and API extraction**
- **📝 README, API docs, and structure analysis**
- **🌍 Ollama-powered translation system**
- **🚀 Complete MCP integration with ai.gpt**
- **📄 Markdown-aware translation preserving structure**
- Test blog with sample content and styling
Planned features:
- AI-powered content enhancement and suggestions
- Automatic translation (ja → en) pipeline
- atproto comment system with OAuth authentication
### 🚧 In Progress
- AI-powered content enhancement pipeline
- atproto OAuth implementation
### 📋 Planned Features
- Advanced template customization
- Plugin system for extensibility
- Real-time comment system
- Multi-blog management
- VTuber integration (ai.verse connection)
## Integration with ai Ecosystem
### System Dependencies
- **ai.gpt**: Memory system, relationship tracking, AI provider
- **ai.card**: Future cross-system content sharing
- **ai.bot**: atproto posting and mention handling
- **ai.verse**: 3D world blog representation (future)
### yui System Compliance
- **Uniqueness**: Each blog post tied to individual identity
- **Reality Reflection**: Personal memories → digital content
- **Irreversibility**: Published content maintains historical integrity
## Getting Started
### 1. Standalone Usage
```bash
git clone [repository]
cd log
cargo run -- init my-blog
cargo run -- new "First Post"
cargo run -- build
cargo run -- serve
```
### 2. AI Ecosystem Integration
```bash
# Start ai.log MCP server
cargo run -- mcp --port 8002
# In another terminal, start ai.gpt
cd ../
# ai.gpt startup commands
# Use Claude Code with natural language blog commands
```
## Documentation Generation Features
### 📚 Automatic README Generation
```bash
# Generate README from project analysis
ailog doc readme --source ./src --with-ai
# Output: Enhanced README.md with:
# - Project overview and metrics
# - Dependency analysis
# - Module structure
# - AI-generated insights
```
### 📖 API Documentation
```bash
# Generate comprehensive API docs
ailog doc api --source ./src --format markdown --output ./docs
# Creates:
# - docs/api.md (main API overview)
# - docs/module_name.md (per-module documentation)
# - Function signatures and documentation
# - Struct/enum definitions
```
### 🏗️ Project Structure Analysis
```bash
# Analyze and document project structure
ailog doc structure --source . --include-deps
# Generates:
# - Directory tree visualization
# - File distribution by language
# - Dependency graph analysis
# - Code metrics and statistics
```
### 📝 Git Changelog Generation
```bash
# Generate changelog from git history
ailog doc changelog --from v1.0.0 --explain-changes
# Creates:
# - Structured changelog
# - Commit categorization
# - AI-enhanced change explanations
```
### 🤖 AI-Enhanced Documentation
When `--with-ai` is enabled:
- **Content Enhancement**: AI improves readability and adds insights
- **Context Awareness**: Leverages ai.gpt memory system
- **Smart Categorization**: Automatic organization of content
- **Technical Writing**: Professional documentation style
## 🌍 Translation System
### Ollama-Powered Translation
ai.log includes a comprehensive translation system powered by Ollama AI models:
```bash
# Basic translation
ailog doc translate --input README.md --target-lang en
# Advanced translation with custom settings
ailog doc translate \
--input docs/technical-guide.ja.md \
--target-lang en \
--source-lang ja \
--output docs/technical-guide.en.md \
--model qwen2.5:latest \
--ollama-endpoint http://localhost:11434
```
### Translation Features
#### 📄 Markdown-Aware Processing
- **Code Block Preservation**: All code snippets remain untranslated
- **Link Maintenance**: URLs and link structures preserved
- **Image Handling**: Alt text can be translated while preserving image paths
- **Table Translation**: Table content translated while maintaining structure
- **Header Preservation**: Markdown headers translated with level maintenance
#### 🎯 Smart Language Detection
- **Auto-Detection**: Automatically detects Japanese content using Unicode ranges
- **Manual Override**: Specify source language for precise control
- **Mixed Content**: Handles documents with multiple languages
#### 🔧 Flexible Configuration
- **Model Selection**: Choose from available Ollama models
- **Custom Endpoints**: Use different Ollama instances
- **Output Control**: Auto-generate or specify output paths
- **Batch Processing**: Process multiple files efficiently
### Supported Languages
| Language | Code | Direction | Model Optimized |
|----------|------|-----------|-----------------|
| English | `en` | ↔️ | ✅ qwen2.5 |
| Japanese | `ja` | ↔️ | ✅ qwen2.5 |
| Chinese | `zh` | ↔️ | ✅ qwen2.5 |
| Korean | `ko` | ↔️ | ⚠️ Basic |
| Spanish | `es` | ↔️ | ⚠️ Basic |
### Translation Workflow
1. **Parse Document**: Analyze markdown structure and identify sections
2. **Preserve Code**: Isolate code blocks and technical content
3. **Translate Content**: Process text sections with Ollama AI
4. **Reconstruct**: Rebuild document maintaining original formatting
5. **Validate**: Ensure structural integrity and completeness
### Integration with ai.gpt
```python
# Via ai.gpt MCP tools
await log_translate_document(
input_file="README.ja.md",
target_lang="en",
model="qwen2.5:latest"
)
```
### Requirements
- **Ollama**: Install and run Ollama locally
- **Models**: Download supported models (qwen2.5:latest recommended)
- **Memory**: Sufficient RAM for model inference
- **Network**: For initial model download only
## Configuration Examples
### Basic Blog Config
```toml
[blog]
title = "My AI Blog"
description = "Personal thoughts and AI insights"
base_url = "https://myblog.example.com"
[ai]
provider = "openai"
model = "gpt-4"
translation = true
```
### Advanced Integration
```json
// ../config.json (ai.gpt)
{
"mcp": {
"servers": {
"ai_gpt": {
"endpoints": {
"log_ai_content": "/log_ai_content",
"log_create_post": "/log_create_post"
}
}
}
}
}
```
## Troubleshooting
### MCP Connection Issues
- Ensure ai.log server is running: `cargo run -- mcp --port 8002`
- Check ai.gpt config includes log endpoints
- Verify `./log/` directory exists relative to ai.gpt
### Build Failures
- Check Rust version: `rustc --version`
- Update dependencies: `cargo update`
- Clear cache: `cargo clean`
### AI Integration Problems
- Verify ai.gpt memory system is initialized
- Check AI provider configuration
- Ensure sufficient context in memory system
## License
© syui
---
**Part of the ai ecosystem**: ai.gpt, ai.card, ai.log, ai.bot, ai.verse, ai.shell

View File

@ -14,24 +14,35 @@ ai.logをai.gptと連携するためのMCPサーバー設定ガイド
## ai.gptでの設定
ai.gptの設定ファイル `~/.config/syui/ai/gpt/config.json` に以下を追加
ai.logツールはai.gptのMCPサーバーに統合済みです。`config.json`に以下の設定が含まれています
```json
{
"mcp": {
"enabled": true,
"enabled": "true",
"auto_detect": "true",
"servers": {
"ai_gpt": {"base_url": "http://localhost:8001"},
"ai_card": {"base_url": "http://localhost:8000"},
"ai_log": {"base_url": "http://localhost:8002"}
"ai_gpt": {
"base_url": "http://localhost:8001",
"endpoints": {
"log_create_post": "/log_create_post",
"log_list_posts": "/log_list_posts",
"log_build_blog": "/log_build_blog",
"log_get_post": "/log_get_post",
"log_system_status": "/log_system_status",
"log_ai_content": "/log_ai_content"
}
}
}
}
}
```
## 利用可能なMCPツール
**重要**: ai.logツールを使用するには、ai.logディレクトリが `./log/` に存在し、ai.logのMCPサーバーがポート8002で稼働している必要があります。
### 1. create_blog_post
## 利用可能なMCPツールai.gpt統合版
### 1. log_create_post
新しいブログ記事を作成します。
**パラメータ**:
@ -42,34 +53,45 @@ ai.gptの設定ファイル `~/.config/syui/ai/gpt/config.json` に以下を追
**使用例**:
```python
# ai.gptからの呼び出し例
result = await mcp_client.call_tool("create_blog_post", {
"title": "AI統合の新しい可能性",
"content": "# 概要\n\nai.gptとai.logの連携により...",
"tags": ["AI", "技術", "ブログ"]
})
# Claude Code/ai.gptから自動呼び出し
# "ブログ記事を書いて"という発言で自動トリガー
```
### 2. list_blog_posts
### 2. log_list_posts
既存のブログ記事一覧を取得します。
**パラメータ**:
- `limit` (オプション): 取得件数上限 (デフォルト: 10)
- `offset` (オプション): スキップ件数 (デフォルト: 0)
### 3. build_blog
### 3. log_build_blog
ブログをビルドして静的ファイルを生成します。
**パラメータ**:
- `enable_ai` (オプション): AI機能を有効化
- `translate` (オプション): 自動翻訳を有効化
- `enable_ai` (オプション): AI機能を有効化 (デフォルト: true)
- `translate` (オプション): 自動翻訳を有効化 (デフォルト: false)
### 4. get_post_content
### 4. log_get_post
指定したスラッグの記事内容を取得します。
**パラメータ**:
- `slug` (必須): 記事のスラッグ
### 5. log_system_status
ai.logシステムの状態を確認します。
### 6. log_ai_content ⭐ NEW
AI記憶システムと連携して自動でブログ記事を生成・投稿します。
**パラメータ**:
- `user_id` (必須): ユーザーID
- `topic` (オプション): 記事のトピック (デフォルト: "daily thoughts")
**機能**:
- ai.gptの記憶システムから関連する思い出を取得
- AI技術で記憶をブログ記事に変換
- 自動でai.logに投稿
## ai.gptからの連携パターン
### 記事の自動投稿

313
src/analyzer/mod.rs Normal file
View File

@ -0,0 +1,313 @@
pub mod rust_analyzer;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfo {
pub name: String,
pub description: Option<String>,
pub version: String,
pub authors: Vec<String>,
pub license: Option<String>,
pub dependencies: HashMap<String, String>,
pub modules: Vec<ModuleInfo>,
pub structure: ProjectStructure,
pub metrics: ProjectMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleInfo {
pub name: String,
pub path: PathBuf,
pub functions: Vec<FunctionInfo>,
pub structs: Vec<StructInfo>,
pub enums: Vec<EnumInfo>,
pub traits: Vec<TraitInfo>,
pub docs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionInfo {
pub name: String,
pub visibility: String,
pub is_async: bool,
pub parameters: Vec<Parameter>,
pub return_type: Option<String>,
pub docs: Option<String>,
pub line_number: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub param_type: String,
pub is_mutable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructInfo {
pub name: String,
pub visibility: String,
pub fields: Vec<FieldInfo>,
pub docs: Option<String>,
pub line_number: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldInfo {
pub name: String,
pub field_type: String,
pub visibility: String,
pub docs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnumInfo {
pub name: String,
pub visibility: String,
pub variants: Vec<VariantInfo>,
pub docs: Option<String>,
pub line_number: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariantInfo {
pub name: String,
pub fields: Vec<FieldInfo>,
pub docs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraitInfo {
pub name: String,
pub visibility: String,
pub methods: Vec<FunctionInfo>,
pub docs: Option<String>,
pub line_number: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectStructure {
pub directories: Vec<DirectoryInfo>,
pub files: Vec<FileInfo>,
pub dependency_graph: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryInfo {
pub name: String,
pub path: PathBuf,
pub file_count: usize,
pub subdirectories: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub name: String,
pub path: PathBuf,
pub language: String,
pub lines_of_code: usize,
pub is_test: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMetrics {
pub total_lines: usize,
pub total_files: usize,
pub test_files: usize,
pub dependency_count: usize,
pub complexity_score: f32,
pub test_coverage: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiInfo {
pub modules: Vec<ModuleInfo>,
pub public_functions: Vec<FunctionInfo>,
pub public_structs: Vec<StructInfo>,
pub public_enums: Vec<EnumInfo>,
pub public_traits: Vec<TraitInfo>,
}
pub struct CodeAnalyzer {
rust_analyzer: rust_analyzer::RustAnalyzer,
}
impl CodeAnalyzer {
pub fn new() -> Self {
Self {
rust_analyzer: rust_analyzer::RustAnalyzer::new(),
}
}
pub fn analyze_project(&self, path: &Path) -> Result<ProjectInfo> {
println!(" 🔍 Analyzing project at: {}", path.display());
// Check if this is a Rust project
let cargo_toml = path.join("Cargo.toml");
if cargo_toml.exists() {
return self.rust_analyzer.analyze_project(path);
}
// For now, only support Rust projects
anyhow::bail!("Only Rust projects are currently supported");
}
pub fn analyze_api(&self, path: &Path) -> Result<ApiInfo> {
println!(" 📚 Analyzing API at: {}", path.display());
let project_info = self.analyze_project(path.parent().unwrap_or(path))?;
// Extract only public items
let mut public_functions = Vec::new();
let mut public_structs = Vec::new();
let mut public_enums = Vec::new();
let mut public_traits = Vec::new();
for module in &project_info.modules {
for func in &module.functions {
if func.visibility == "pub" {
public_functions.push(func.clone());
}
}
for struct_info in &module.structs {
if struct_info.visibility == "pub" {
public_structs.push(struct_info.clone());
}
}
for enum_info in &module.enums {
if enum_info.visibility == "pub" {
public_enums.push(enum_info.clone());
}
}
for trait_info in &module.traits {
if trait_info.visibility == "pub" {
public_traits.push(trait_info.clone());
}
}
}
Ok(ApiInfo {
modules: project_info.modules,
public_functions,
public_structs,
public_enums,
public_traits,
})
}
pub fn analyze_structure(&self, path: &Path, include_deps: bool) -> Result<ProjectStructure> {
println!(" 🏗️ Analyzing structure at: {}", path.display());
let mut directories = Vec::new();
let mut files = Vec::new();
let mut dependency_graph = HashMap::new();
self.walk_directory(path, &mut directories, &mut files)?;
if include_deps {
dependency_graph = self.analyze_dependencies(path)?;
}
Ok(ProjectStructure {
directories,
files,
dependency_graph,
})
}
fn walk_directory(
&self,
path: &Path,
directories: &mut Vec<DirectoryInfo>,
files: &mut Vec<FileInfo>,
) -> Result<()> {
use walkdir::WalkDir;
let walker = WalkDir::new(path)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
// Skip hidden files and common build/cache directories
!name.starts_with('.')
&& name != "target"
&& name != "node_modules"
&& name != "dist"
});
for entry in walker {
let entry = entry?;
let path = entry.path();
let relative_path = path.strip_prefix(path.ancestors().last().unwrap())?;
if entry.file_type().is_dir() {
let file_count = std::fs::read_dir(path)?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false))
.count();
let subdirectories = std::fs::read_dir(path)?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
directories.push(DirectoryInfo {
name: path.file_name().unwrap().to_string_lossy().to_string(),
path: relative_path.to_path_buf(),
file_count,
subdirectories,
});
} else if entry.file_type().is_file() {
let language = self.detect_language(path);
let lines_of_code = self.count_lines(path)?;
let is_test = self.is_test_file(path);
files.push(FileInfo {
name: path.file_name().unwrap().to_string_lossy().to_string(),
path: relative_path.to_path_buf(),
language,
lines_of_code,
is_test,
});
}
}
Ok(())
}
fn detect_language(&self, path: &Path) -> String {
match path.extension().and_then(|s| s.to_str()) {
Some("rs") => "rust".to_string(),
Some("py") => "python".to_string(),
Some("js") => "javascript".to_string(),
Some("ts") => "typescript".to_string(),
Some("md") => "markdown".to_string(),
Some("toml") => "toml".to_string(),
Some("json") => "json".to_string(),
Some("yaml") | Some("yml") => "yaml".to_string(),
_ => "unknown".to_string(),
}
}
fn count_lines(&self, path: &Path) -> Result<usize> {
let content = std::fs::read_to_string(path)?;
Ok(content.lines().count())
}
fn is_test_file(&self, path: &Path) -> bool {
let filename = path.file_name().unwrap().to_string_lossy();
filename.contains("test")
|| filename.starts_with("test_")
|| path.to_string_lossy().contains("/tests/")
}
fn analyze_dependencies(&self, _path: &Path) -> Result<HashMap<String, Vec<String>>> {
// For now, just return empty dependencies
// TODO: Implement actual dependency analysis
Ok(HashMap::new())
}
}

View File

@ -0,0 +1,512 @@
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use syn::{visit::Visit, ItemEnum, ItemFn, ItemStruct, ItemTrait, Visibility};
use super::*;
pub struct RustAnalyzer;
impl RustAnalyzer {
pub fn new() -> Self {
Self
}
pub fn analyze_project(&self, path: &Path) -> Result<ProjectInfo> {
// Parse Cargo.toml
let cargo_toml_path = path.join("Cargo.toml");
let cargo_content = std::fs::read_to_string(&cargo_toml_path)?;
let cargo_toml: toml::Value = toml::from_str(&cargo_content)?;
let package = cargo_toml.get("package").unwrap();
let name = package.get("name").unwrap().as_str().unwrap().to_string();
let description = package.get("description").map(|v| v.as_str().unwrap().to_string());
let version = package.get("version").unwrap().as_str().unwrap().to_string();
let authors = package
.get("authors")
.map(|v| {
v.as_array()
.unwrap()
.iter()
.map(|a| a.as_str().unwrap().to_string())
.collect()
})
.unwrap_or_default();
let license = package.get("license").map(|v| v.as_str().unwrap().to_string());
// Parse dependencies
let dependencies = self.parse_dependencies(&cargo_toml)?;
// Analyze source code
let src_path = path.join("src");
let modules = self.analyze_modules(&src_path)?;
// Calculate metrics
let metrics = self.calculate_metrics(&modules, &dependencies);
// Analyze structure
let structure = self.analyze_project_structure(path)?;
Ok(ProjectInfo {
name,
description,
version,
authors,
license,
dependencies,
modules,
structure,
metrics,
})
}
fn parse_dependencies(&self, cargo_toml: &toml::Value) -> Result<HashMap<String, String>> {
let mut dependencies = HashMap::new();
if let Some(deps) = cargo_toml.get("dependencies") {
if let Some(deps_table) = deps.as_table() {
for (name, value) in deps_table {
let version = match value {
toml::Value::String(v) => v.clone(),
toml::Value::Table(t) => {
t.get("version")
.and_then(|v| v.as_str())
.unwrap_or("*")
.to_string()
}
_ => "*".to_string(),
};
dependencies.insert(name.clone(), version);
}
}
}
Ok(dependencies)
}
fn analyze_modules(&self, src_path: &Path) -> Result<Vec<ModuleInfo>> {
let mut modules = Vec::new();
if !src_path.exists() {
return Ok(modules);
}
// Walk through all .rs files
for entry in walkdir::WalkDir::new(src_path) {
let entry = entry?;
if entry.file_type().is_file() {
if let Some(extension) = entry.path().extension() {
if extension == "rs" {
if let Ok(module) = self.analyze_rust_file(entry.path()) {
modules.push(module);
}
}
}
}
}
Ok(modules)
}
fn analyze_rust_file(&self, file_path: &Path) -> Result<ModuleInfo> {
let content = std::fs::read_to_string(file_path)?;
let syntax_tree = syn::parse_file(&content)?;
let mut visitor = RustVisitor::new();
visitor.visit_file(&syntax_tree);
let module_name = file_path
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
// Extract module-level documentation
let docs = self.extract_module_docs(&content);
Ok(ModuleInfo {
name: module_name,
path: file_path.to_path_buf(),
functions: visitor.functions,
structs: visitor.structs,
enums: visitor.enums,
traits: visitor.traits,
docs,
})
}
fn extract_module_docs(&self, content: &str) -> Option<String> {
let lines: Vec<&str> = content.lines().collect();
let mut doc_lines = Vec::new();
let mut in_module_doc = false;
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with("//!") {
in_module_doc = true;
doc_lines.push(trimmed.trim_start_matches("//!").trim());
} else if trimmed.starts_with("/*!") {
in_module_doc = true;
let content = trimmed.trim_start_matches("/*!").trim_end_matches("*/").trim();
doc_lines.push(content);
} else if in_module_doc && !trimmed.is_empty() && !trimmed.starts_with("//") {
break;
}
}
if doc_lines.is_empty() {
None
} else {
Some(doc_lines.join("\n"))
}
}
fn calculate_metrics(&self, modules: &[ModuleInfo], dependencies: &HashMap<String, String>) -> ProjectMetrics {
let total_lines = modules.iter().map(|m| {
std::fs::read_to_string(&m.path)
.map(|content| content.lines().count())
.unwrap_or(0)
}).sum();
let total_files = modules.len();
let test_files = modules.iter().filter(|m| {
m.name.contains("test") || m.path.to_string_lossy().contains("/tests/")
}).count();
let dependency_count = dependencies.len();
// Simple complexity calculation based on number of functions and structs
let complexity_score = modules.iter().map(|m| {
(m.functions.len() + m.structs.len() + m.enums.len() + m.traits.len()) as f32
}).sum::<f32>() / modules.len().max(1) as f32;
ProjectMetrics {
total_lines,
total_files,
test_files,
dependency_count,
complexity_score,
test_coverage: None, // TODO: Implement test coverage calculation
}
}
fn analyze_project_structure(&self, path: &Path) -> Result<ProjectStructure> {
let mut directories = Vec::new();
let mut files = Vec::new();
self.walk_directory(path, &mut directories, &mut files)?;
Ok(ProjectStructure {
directories,
files,
dependency_graph: HashMap::new(), // TODO: Implement dependency graph
})
}
fn walk_directory(
&self,
path: &Path,
directories: &mut Vec<DirectoryInfo>,
files: &mut Vec<FileInfo>,
) -> Result<()> {
for entry in walkdir::WalkDir::new(path).max_depth(3) {
let entry = entry?;
let relative_path = entry.path().strip_prefix(path)?;
if entry.file_type().is_dir() && relative_path != Path::new("") {
let file_count = std::fs::read_dir(entry.path())?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false))
.count();
let subdirectories = std::fs::read_dir(entry.path())?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
directories.push(DirectoryInfo {
name: entry.path().file_name().unwrap().to_string_lossy().to_string(),
path: relative_path.to_path_buf(),
file_count,
subdirectories,
});
} else if entry.file_type().is_file() {
let language = match entry.path().extension().and_then(|s| s.to_str()) {
Some("rs") => "rust".to_string(),
Some("toml") => "toml".to_string(),
Some("md") => "markdown".to_string(),
_ => "unknown".to_string(),
};
let lines_of_code = std::fs::read_to_string(entry.path())
.map(|content| content.lines().count())
.unwrap_or(0);
let is_test = entry.path().to_string_lossy().contains("test");
files.push(FileInfo {
name: entry.path().file_name().unwrap().to_string_lossy().to_string(),
path: relative_path.to_path_buf(),
language,
lines_of_code,
is_test,
});
}
}
Ok(())
}
}
struct RustVisitor {
functions: Vec<FunctionInfo>,
structs: Vec<StructInfo>,
enums: Vec<EnumInfo>,
traits: Vec<TraitInfo>,
current_line: usize,
}
impl RustVisitor {
fn new() -> Self {
Self {
functions: Vec::new(),
structs: Vec::new(),
enums: Vec::new(),
traits: Vec::new(),
current_line: 1,
}
}
fn visibility_to_string(&self, vis: &Visibility) -> String {
match vis {
Visibility::Public(_) => "pub".to_string(),
Visibility::Restricted(_) => "pub(restricted)".to_string(),
Visibility::Inherited => "private".to_string(),
}
}
fn extract_docs(&self, attrs: &[syn::Attribute]) -> Option<String> {
let mut docs = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
docs.push(lit_str.value());
}
}
}
}
}
if docs.is_empty() {
None
} else {
Some(docs.join("\n"))
}
}
}
impl<'ast> Visit<'ast> for RustVisitor {
fn visit_item_fn(&mut self, node: &'ast ItemFn) {
let name = node.sig.ident.to_string();
let visibility = self.visibility_to_string(&node.vis);
let is_async = node.sig.asyncness.is_some();
let parameters = node.sig.inputs.iter().map(|input| {
match input {
syn::FnArg::Receiver(_) => Parameter {
name: "self".to_string(),
param_type: "Self".to_string(),
is_mutable: false,
},
syn::FnArg::Typed(typed) => {
let name = match &*typed.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => "unknown".to_string(),
};
Parameter {
name,
param_type: quote::quote!(#typed.ty).to_string(),
is_mutable: false, // TODO: Detect mutability
}
}
}
}).collect();
let return_type = match &node.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ty) => Some(quote::quote!(#ty).to_string()),
};
let docs = self.extract_docs(&node.attrs);
self.functions.push(FunctionInfo {
name,
visibility,
is_async,
parameters,
return_type,
docs,
line_number: self.current_line,
});
syn::visit::visit_item_fn(self, node);
}
fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
let name = node.ident.to_string();
let visibility = self.visibility_to_string(&node.vis);
let docs = self.extract_docs(&node.attrs);
let fields = match &node.fields {
syn::Fields::Named(fields) => {
fields.named.iter().map(|field| {
FieldInfo {
name: field.ident.as_ref().unwrap().to_string(),
field_type: quote::quote!(#field.ty).to_string(),
visibility: self.visibility_to_string(&field.vis),
docs: self.extract_docs(&field.attrs),
}
}).collect()
}
syn::Fields::Unnamed(fields) => {
fields.unnamed.iter().enumerate().map(|(i, field)| {
FieldInfo {
name: format!("field_{}", i),
field_type: quote::quote!(#field.ty).to_string(),
visibility: self.visibility_to_string(&field.vis),
docs: self.extract_docs(&field.attrs),
}
}).collect()
}
syn::Fields::Unit => Vec::new(),
};
self.structs.push(StructInfo {
name,
visibility,
fields,
docs,
line_number: self.current_line,
});
syn::visit::visit_item_struct(self, node);
}
fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
let name = node.ident.to_string();
let visibility = self.visibility_to_string(&node.vis);
let docs = self.extract_docs(&node.attrs);
let variants = node.variants.iter().map(|variant| {
let variant_name = variant.ident.to_string();
let variant_docs = self.extract_docs(&variant.attrs);
let fields = match &variant.fields {
syn::Fields::Named(fields) => {
fields.named.iter().map(|field| {
FieldInfo {
name: field.ident.as_ref().unwrap().to_string(),
field_type: quote::quote!(#field.ty).to_string(),
visibility: self.visibility_to_string(&field.vis),
docs: self.extract_docs(&field.attrs),
}
}).collect()
}
syn::Fields::Unnamed(fields) => {
fields.unnamed.iter().enumerate().map(|(i, field)| {
FieldInfo {
name: format!("field_{}", i),
field_type: quote::quote!(#field.ty).to_string(),
visibility: self.visibility_to_string(&field.vis),
docs: self.extract_docs(&field.attrs),
}
}).collect()
}
syn::Fields::Unit => Vec::new(),
};
VariantInfo {
name: variant_name,
fields,
docs: variant_docs,
}
}).collect();
self.enums.push(EnumInfo {
name,
visibility,
variants,
docs,
line_number: self.current_line,
});
syn::visit::visit_item_enum(self, node);
}
fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
let name = node.ident.to_string();
let visibility = self.visibility_to_string(&node.vis);
let docs = self.extract_docs(&node.attrs);
let methods = node.items.iter().filter_map(|item| {
match item {
syn::TraitItem::Fn(method) => {
let method_name = method.sig.ident.to_string();
let method_visibility = "pub".to_string(); // Trait methods are inherently public
let is_async = method.sig.asyncness.is_some();
let parameters = method.sig.inputs.iter().map(|input| {
match input {
syn::FnArg::Receiver(_) => Parameter {
name: "self".to_string(),
param_type: "Self".to_string(),
is_mutable: false,
},
syn::FnArg::Typed(typed) => {
let name = match &*typed.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => "unknown".to_string(),
};
Parameter {
name,
param_type: quote::quote!(#typed.ty).to_string(),
is_mutable: false,
}
}
}
}).collect();
let return_type = match &method.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ty) => Some(quote::quote!(#ty).to_string()),
};
let method_docs = self.extract_docs(&method.attrs);
Some(FunctionInfo {
name: method_name,
visibility: method_visibility,
is_async,
parameters,
return_type,
docs: method_docs,
line_number: self.current_line,
})
}
_ => None,
}
}).collect();
self.traits.push(TraitInfo {
name,
visibility,
methods,
docs,
line_number: self.current_line,
});
syn::visit::visit_item_trait(self, node);
}
}

287
src/commands/doc.rs Normal file
View File

@ -0,0 +1,287 @@
use anyhow::Result;
use clap::{Subcommand, Parser};
use std::path::PathBuf;
use crate::analyzer::CodeAnalyzer;
use crate::doc_generator::DocGenerator;
use crate::translator::{TranslationConfig, Translator};
use crate::translator::ollama_translator::OllamaTranslator;
#[derive(Parser)]
#[command(about = "Generate documentation from code")]
pub struct DocCommand {
#[command(subcommand)]
pub action: DocAction,
}
#[derive(Subcommand)]
pub enum DocAction {
/// Generate README.md from project analysis
Readme {
/// Source directory to analyze
#[arg(long, default_value = ".")]
source: PathBuf,
/// Output file path
#[arg(long, default_value = "README.md")]
output: PathBuf,
/// Include AI-generated insights
#[arg(long)]
with_ai: bool,
},
/// Generate API documentation
Api {
/// Source directory to analyze
#[arg(long, default_value = "./src")]
source: PathBuf,
/// Output directory
#[arg(long, default_value = "./docs")]
output: PathBuf,
/// Output format (markdown, html, json)
#[arg(long, default_value = "markdown")]
format: String,
},
/// Analyze and document project structure
Structure {
/// Source directory to analyze
#[arg(long, default_value = ".")]
source: PathBuf,
/// Output file path
#[arg(long, default_value = "docs/structure.md")]
output: PathBuf,
/// Include dependency graph
#[arg(long)]
include_deps: bool,
},
/// Generate changelog from git commits
Changelog {
/// Start from this commit/tag
#[arg(long)]
from: Option<String>,
/// End at this commit/tag
#[arg(long)]
to: Option<String>,
/// Output file path
#[arg(long, default_value = "CHANGELOG.md")]
output: PathBuf,
/// Include AI explanations for changes
#[arg(long)]
explain_changes: bool,
},
/// Translate documentation using Ollama
Translate {
/// Input file path
#[arg(long)]
input: PathBuf,
/// Target language (en, ja, zh, ko, es)
#[arg(long)]
target_lang: String,
/// Source language (auto-detect if not specified)
#[arg(long)]
source_lang: Option<String>,
/// Output file path (auto-generated if not specified)
#[arg(long)]
output: Option<PathBuf>,
/// Ollama model to use
#[arg(long, default_value = "qwen2.5:latest")]
model: String,
/// Ollama endpoint
#[arg(long, default_value = "http://localhost:11434")]
ollama_endpoint: String,
},
}
impl DocCommand {
pub async fn execute(self, base_path: PathBuf) -> Result<()> {
match self.action {
DocAction::Readme { ref source, ref output, with_ai } => {
self.generate_readme(base_path, source.clone(), output.clone(), with_ai).await
}
DocAction::Api { ref source, ref output, ref format } => {
self.generate_api_docs(base_path, source.clone(), output.clone(), format.clone()).await
}
DocAction::Structure { ref source, ref output, include_deps } => {
self.analyze_structure(base_path, source.clone(), output.clone(), include_deps).await
}
DocAction::Changelog { ref from, ref to, ref output, explain_changes } => {
self.generate_changelog(base_path, from.clone(), to.clone(), output.clone(), explain_changes).await
}
DocAction::Translate { ref input, ref target_lang, ref source_lang, ref output, ref model, ref ollama_endpoint } => {
self.translate_document(input.clone(), target_lang.clone(), source_lang.clone(), output.clone(), model.clone(), ollama_endpoint.clone()).await
}
}
}
async fn generate_readme(
&self,
base_path: PathBuf,
source: PathBuf,
output: PathBuf,
with_ai: bool,
) -> Result<()> {
println!("🔍 Analyzing project for README generation...");
let analyzer = CodeAnalyzer::new();
let generator = DocGenerator::new(base_path.clone(), with_ai);
let project_info = analyzer.analyze_project(&source)?;
let readme_content = generator.generate_readme(&project_info).await?;
std::fs::write(&output, readme_content)?;
println!("✅ README generated: {}", output.display());
Ok(())
}
async fn generate_api_docs(
&self,
base_path: PathBuf,
source: PathBuf,
output: PathBuf,
format: String,
) -> Result<()> {
println!("📚 Generating API documentation...");
let analyzer = CodeAnalyzer::new();
let generator = DocGenerator::new(base_path.clone(), true);
let api_info = analyzer.analyze_api(&source)?;
match format.as_str() {
"markdown" => {
let docs = generator.generate_api_markdown(&api_info).await?;
std::fs::create_dir_all(&output)?;
for (filename, content) in docs {
let file_path = output.join(filename);
std::fs::write(&file_path, content)?;
println!(" 📄 Generated: {}", file_path.display());
}
}
"html" => {
println!("HTML format not yet implemented");
}
"json" => {
let json_content = serde_json::to_string_pretty(&api_info)?;
let file_path = output.join("api.json");
std::fs::create_dir_all(&output)?;
std::fs::write(&file_path, json_content)?;
println!(" 📄 Generated: {}", file_path.display());
}
_ => {
anyhow::bail!("Unsupported format: {}", format);
}
}
println!("✅ API documentation generated in: {}", output.display());
Ok(())
}
async fn analyze_structure(
&self,
base_path: PathBuf,
source: PathBuf,
output: PathBuf,
include_deps: bool,
) -> Result<()> {
println!("🏗️ Analyzing project structure...");
let analyzer = CodeAnalyzer::new();
let generator = DocGenerator::new(base_path.clone(), false);
let structure = analyzer.analyze_structure(&source, include_deps)?;
let structure_doc = generator.generate_structure_doc(&structure).await?;
// Ensure output directory exists
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&output, structure_doc)?;
println!("✅ Structure documentation generated: {}", output.display());
Ok(())
}
async fn generate_changelog(
&self,
base_path: PathBuf,
from: Option<String>,
to: Option<String>,
output: PathBuf,
explain_changes: bool,
) -> Result<()> {
println!("📝 Generating changelog from git history...");
let generator = DocGenerator::new(base_path.clone(), explain_changes);
let changelog = generator.generate_changelog(from, to).await?;
std::fs::write(&output, changelog)?;
println!("✅ Changelog generated: {}", output.display());
Ok(())
}
async fn translate_document(
&self,
input: PathBuf,
target_lang: String,
source_lang: Option<String>,
output: Option<PathBuf>,
model: String,
ollama_endpoint: String,
) -> Result<()> {
println!("🌍 Translating document with Ollama...");
// Read input file
let content = std::fs::read_to_string(&input)?;
println!("📖 Read {} characters from {}", content.len(), input.display());
// Setup translation config
let config = TranslationConfig {
source_lang: source_lang.unwrap_or_else(|| {
// Simple language detection based on content
if content.chars().any(|c| {
(c >= '\u{3040}' && c <= '\u{309F}') || // Hiragana
(c >= '\u{30A0}' && c <= '\u{30FF}') || // Katakana
(c >= '\u{4E00}' && c <= '\u{9FAF}') // CJK Unified Ideographs
}) {
"ja".to_string()
} else {
"en".to_string()
}
}),
target_lang,
ollama_endpoint,
model,
preserve_code: true,
preserve_links: true,
};
println!("🔧 Translation config: {}{}", config.source_lang, config.target_lang);
println!("🤖 Using model: {} at {}", config.model, config.ollama_endpoint);
// Create translator
let translator = OllamaTranslator::new();
// Perform translation
let translated = translator.translate_markdown(&content, &config).await?;
// Determine output path
let output_path = match output {
Some(path) => path,
None => {
let input_stem = input.file_stem().unwrap().to_string_lossy();
let input_ext = input.extension().unwrap_or_default().to_string_lossy();
let output_name = format!("{}.{}.{}", input_stem, config.target_lang, input_ext);
input.parent().unwrap_or_else(|| std::path::Path::new(".")).join(output_name)
}
};
// Write translated content
std::fs::write(&output_path, translated)?;
println!("✅ Translation completed: {}", output_path.display());
println!("📝 Language: {}{}", config.source_lang, config.target_lang);
Ok(())
}
}

View File

@ -2,4 +2,5 @@ pub mod init;
pub mod build;
pub mod new;
pub mod serve;
pub mod clean;
pub mod clean;
pub mod doc;

235
src/doc_generator.rs Normal file
View File

@ -0,0 +1,235 @@
use anyhow::Result;
use std::path::PathBuf;
use crate::analyzer::{ProjectInfo, ApiInfo, ProjectStructure};
use crate::ai::gpt_client::GptClient;
pub struct DocGenerator {
base_path: PathBuf,
ai_enabled: bool,
templates: DocTemplates,
}
pub struct DocTemplates {
readme_template: String,
api_template: String,
structure_template: String,
changelog_template: String,
}
impl DocGenerator {
pub fn new(base_path: PathBuf, ai_enabled: bool) -> Self {
let templates = DocTemplates::default();
Self {
base_path,
ai_enabled,
templates,
}
}
pub async fn generate_readme(&self, project_info: &ProjectInfo) -> Result<String> {
let mut content = self.templates.readme_template.clone();
// Simple template substitution
content = content.replace("{{name}}", &project_info.name);
content = content.replace("{{description}}",
&project_info.description.as_ref().unwrap_or(&"A Rust project".to_string()));
content = content.replace("{{module_count}}", &project_info.modules.len().to_string());
content = content.replace("{{total_lines}}", &project_info.metrics.total_lines.to_string());
let deps = project_info.dependencies.iter()
.map(|(name, version)| format!("- {}: {}", name, version))
.collect::<Vec<_>>()
.join("\n");
content = content.replace("{{dependencies}}", &deps);
content = content.replace("{{license}}",
&project_info.license.as_ref().unwrap_or(&"MIT".to_string()));
if self.ai_enabled {
content = self.enhance_with_ai(&content, "readme").await?;
}
Ok(content)
}
pub async fn generate_api_markdown(&self, api_info: &ApiInfo) -> Result<Vec<(String, String)>> {
let mut files = Vec::new();
// Generate main API documentation
let main_content = self.templates.api_template.replace("{{content}}", "Generated API Documentation");
files.push(("api.md".to_string(), main_content));
// Generate individual module docs
for module in &api_info.modules {
if !module.functions.is_empty() || !module.structs.is_empty() {
let module_content = self.generate_module_doc(module).await?;
files.push((format!("{}.md", module.name), module_content));
}
}
Ok(files)
}
pub async fn generate_structure_doc(&self, structure: &ProjectStructure) -> Result<String> {
let content = self.templates.structure_template.replace("{{content}}",
&format!("Found {} directories and {} files",
structure.directories.len(),
structure.files.len()));
Ok(content)
}
pub async fn generate_changelog(&self, from: Option<String>, to: Option<String>) -> Result<String> {
let commits = self.get_git_commits(from, to)?;
let mut content = self.templates.changelog_template.replace("{{content}}",
&format!("Found {} commits", commits.len()));
if self.ai_enabled {
content = self.enhance_changelog_with_ai(&content, &commits).await?;
}
Ok(content)
}
async fn enhance_with_ai(&self, content: &str, doc_type: &str) -> Result<String> {
if !self.ai_enabled {
return Ok(content.to_string());
}
let gpt_client = GptClient::new(
std::env::var("OPENAI_API_KEY").unwrap_or_default(),
None,
);
let prompt = format!(
"Enhance this {} documentation with additional insights and improve readability:\n\n{}",
doc_type, content
);
match gpt_client.chat("You are a technical writer helping to improve documentation.", &prompt).await {
Ok(enhanced) => Ok(enhanced),
Err(_) => Ok(content.to_string()), // Fallback to original content
}
}
async fn generate_module_doc(&self, module: &crate::analyzer::ModuleInfo) -> Result<String> {
let mut content = format!("# Module: {}\n\n", module.name);
if let Some(docs) = &module.docs {
content.push_str(&format!("{}\n\n", docs));
}
// Add functions
if !module.functions.is_empty() {
content.push_str("## Functions\n\n");
for func in &module.functions {
content.push_str(&self.format_function_doc(func));
}
}
// Add structs
if !module.structs.is_empty() {
content.push_str("## Structs\n\n");
for struct_info in &module.structs {
content.push_str(&self.format_struct_doc(struct_info));
}
}
Ok(content)
}
fn format_function_doc(&self, func: &crate::analyzer::FunctionInfo) -> String {
let mut doc = format!("### `{}`\n\n", func.name);
if let Some(docs) = &func.docs {
doc.push_str(&format!("{}\n\n", docs));
}
doc.push_str(&format!("**Visibility:** `{}`\n", func.visibility));
if func.is_async {
doc.push_str("**Async:** Yes\n");
}
if !func.parameters.is_empty() {
doc.push_str("\n**Parameters:**\n");
for param in &func.parameters {
doc.push_str(&format!("- `{}`: `{}`\n", param.name, param.param_type));
}
}
if let Some(return_type) = &func.return_type {
doc.push_str(&format!("\n**Returns:** `{}`\n", return_type));
}
doc.push_str("\n---\n\n");
doc
}
fn format_struct_doc(&self, struct_info: &crate::analyzer::StructInfo) -> String {
let mut doc = format!("### `{}`\n\n", struct_info.name);
if let Some(docs) = &struct_info.docs {
doc.push_str(&format!("{}\n\n", docs));
}
doc.push_str(&format!("**Visibility:** `{}`\n\n", struct_info.visibility));
if !struct_info.fields.is_empty() {
doc.push_str("**Fields:**\n");
for field in &struct_info.fields {
doc.push_str(&format!("- `{}`: `{}` ({})\n", field.name, field.field_type, field.visibility));
if let Some(field_docs) = &field.docs {
doc.push_str(&format!(" - {}\n", field_docs));
}
}
}
doc.push_str("\n---\n\n");
doc
}
async fn enhance_changelog_with_ai(&self, content: &str, _commits: &[GitCommit]) -> Result<String> {
// TODO: Implement AI-enhanced changelog generation
Ok(content.to_string())
}
fn get_git_commits(&self, _from: Option<String>, _to: Option<String>) -> Result<Vec<GitCommit>> {
// TODO: Implement git history parsing
Ok(vec![])
}
}
#[derive(Debug)]
pub struct GitCommit {
pub hash: String,
pub message: String,
pub author: String,
pub date: String,
}
impl DocTemplates {
fn default() -> Self {
Self {
readme_template: r#"# {{name}}
{{description}}
## Overview
This project contains {{module_count}} modules with {{total_lines}} lines of code.
## Dependencies
{{dependencies}}
## License
{{license}}
"#.to_string(),
api_template: "# API Documentation\n\n{{content}}".to_string(),
structure_template: "# Project Structure\n\n{{content}}".to_string(),
changelog_template: "# Changelog\n\n{{content}}".to_string(),
}
}
}

View File

@ -2,10 +2,13 @@ use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
mod analyzer;
mod commands;
mod doc_generator;
mod generator;
mod markdown;
mod template;
mod translator;
mod config;
mod ai;
mod atproto;
@ -59,6 +62,8 @@ enum Commands {
#[arg(default_value = ".")]
path: PathBuf,
},
/// Generate documentation from code
Doc(commands::doc::DocCommand),
}
#[tokio::main]
@ -86,6 +91,9 @@ async fn main() -> Result<()> {
let server = McpServer::new(path);
server.serve(port).await?;
}
Commands::Doc(doc_cmd) => {
doc_cmd.execute(std::env::current_dir()?).await?;
}
}
Ok(())

View File

@ -113,6 +113,12 @@ async fn call_tool(
.ok_or(StatusCode::BAD_REQUEST)?;
state.blog_tools.get_post_content(slug).await
}
"translate_document" => {
state.blog_tools.translate_document(arguments).await
}
"generate_documentation" => {
state.blog_tools.generate_documentation(arguments).await
}
_ => {
return Ok(Json(McpResponse {
jsonrpc: "2.0".to_string(),

View File

@ -217,6 +217,141 @@ impl BlogTools {
})
}
pub async fn translate_document(&self, args: Value) -> Result<ToolResult> {
use crate::commands::doc::DocCommand;
use crate::commands::doc::DocAction;
let input_file = args.get("input_file")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("input_file is required"))?;
let target_lang = args.get("target_lang")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("target_lang is required"))?;
let source_lang = args.get("source_lang").and_then(|v| v.as_str()).map(|s| s.to_string());
let output_file = args.get("output_file").and_then(|v| v.as_str()).map(|s| PathBuf::from(s));
let model = args.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5:latest");
let ollama_endpoint = args.get("ollama_endpoint").and_then(|v| v.as_str()).unwrap_or("http://localhost:11434");
let doc_cmd = DocCommand {
action: DocAction::Translate {
input: PathBuf::from(input_file),
target_lang: target_lang.to_string(),
source_lang: source_lang.clone(),
output: output_file,
model: model.to_string(),
ollama_endpoint: ollama_endpoint.to_string(),
}
};
match doc_cmd.execute(self.base_path.clone()).await {
Ok(_) => {
let output_path = if let Some(output) = args.get("output_file").and_then(|v| v.as_str()) {
output.to_string()
} else {
let input_path = PathBuf::from(input_file);
let stem = input_path.file_stem().unwrap().to_string_lossy();
let ext = input_path.extension().unwrap_or_default().to_string_lossy();
format!("{}.{}.{}", stem, target_lang, ext)
};
Ok(ToolResult {
content: vec![Content {
content_type: "text".to_string(),
text: format!("Document translated successfully from {} to {}. Output: {}",
source_lang.unwrap_or_else(|| "auto-detected".to_string()),
target_lang, output_path),
}],
is_error: None,
})
}
Err(e) => Ok(ToolResult {
content: vec![Content {
content_type: "text".to_string(),
text: format!("Translation failed: {}", e),
}],
is_error: Some(true),
})
}
}
pub async fn generate_documentation(&self, args: Value) -> Result<ToolResult> {
use crate::commands::doc::DocCommand;
use crate::commands::doc::DocAction;
let doc_type = args.get("doc_type")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("doc_type is required"))?;
let source_path = args.get("source_path").and_then(|v| v.as_str()).unwrap_or(".");
let output_path = args.get("output_path").and_then(|v| v.as_str());
let with_ai = args.get("with_ai").and_then(|v| v.as_bool()).unwrap_or(true);
let include_deps = args.get("include_deps").and_then(|v| v.as_bool()).unwrap_or(false);
let format_type = args.get("format_type").and_then(|v| v.as_str()).unwrap_or("markdown");
let action = match doc_type {
"readme" => DocAction::Readme {
source: PathBuf::from(source_path),
output: PathBuf::from(output_path.unwrap_or("README.md")),
with_ai,
},
"api" => DocAction::Api {
source: PathBuf::from(source_path),
output: PathBuf::from(output_path.unwrap_or("./docs")),
format: format_type.to_string(),
},
"structure" => DocAction::Structure {
source: PathBuf::from(source_path),
output: PathBuf::from(output_path.unwrap_or("docs/structure.md")),
include_deps,
},
"changelog" => DocAction::Changelog {
from: None,
to: None,
output: PathBuf::from(output_path.unwrap_or("CHANGELOG.md")),
explain_changes: with_ai,
},
_ => return Ok(ToolResult {
content: vec![Content {
content_type: "text".to_string(),
text: format!("Unsupported doc_type: {}. Supported types: readme, api, structure, changelog", doc_type),
}],
is_error: Some(true),
})
};
let doc_cmd = DocCommand { action };
match doc_cmd.execute(self.base_path.clone()).await {
Ok(_) => {
let output_path = match doc_type {
"readme" => output_path.unwrap_or("README.md"),
"api" => output_path.unwrap_or("./docs"),
"structure" => output_path.unwrap_or("docs/structure.md"),
"changelog" => output_path.unwrap_or("CHANGELOG.md"),
_ => "unknown"
};
Ok(ToolResult {
content: vec![Content {
content_type: "text".to_string(),
text: format!("{} documentation generated successfully. Output: {}",
doc_type.to_uppercase(), output_path),
}],
is_error: None,
})
}
Err(e) => Ok(ToolResult {
content: vec![Content {
content_type: "text".to_string(),
text: format!("Documentation generation failed: {}", e),
}],
is_error: Some(true),
})
}
}
pub fn get_tools() -> Vec<Tool> {
vec![
Tool {
@ -294,6 +429,76 @@ impl BlogTools {
"required": ["slug"]
}),
},
Tool {
name: "translate_document".to_string(),
description: "Translate markdown documents using Ollama AI while preserving structure".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"input_file": {
"type": "string",
"description": "Path to the input markdown file"
},
"target_lang": {
"type": "string",
"description": "Target language code (en, ja, zh, ko, es)"
},
"source_lang": {
"type": "string",
"description": "Source language code (auto-detect if not specified)"
},
"output_file": {
"type": "string",
"description": "Output file path (auto-generated if not specified)"
},
"model": {
"type": "string",
"description": "Ollama model to use (default: qwen2.5:latest)"
},
"ollama_endpoint": {
"type": "string",
"description": "Ollama API endpoint (default: http://localhost:11434)"
}
},
"required": ["input_file", "target_lang"]
}),
},
Tool {
name: "generate_documentation".to_string(),
description: "Generate various types of documentation from code analysis".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"doc_type": {
"type": "string",
"enum": ["readme", "api", "structure", "changelog"],
"description": "Type of documentation to generate"
},
"source_path": {
"type": "string",
"description": "Source directory to analyze (default: current directory)"
},
"output_path": {
"type": "string",
"description": "Output file or directory path"
},
"with_ai": {
"type": "boolean",
"description": "Include AI-generated insights (default: true)"
},
"include_deps": {
"type": "boolean",
"description": "Include dependency analysis (default: false)"
},
"format_type": {
"type": "string",
"enum": ["markdown", "html", "json"],
"description": "Output format (default: markdown)"
}
},
"required": ["doc_type"]
}),
},
]
}
}

View File

@ -0,0 +1,253 @@
use anyhow::Result;
use regex::Regex;
use super::MarkdownSection;
pub struct MarkdownParser {
code_block_regex: Regex,
header_regex: Regex,
link_regex: Regex,
image_regex: Regex,
table_regex: Regex,
list_regex: Regex,
quote_regex: Regex,
}
impl MarkdownParser {
pub fn new() -> Self {
Self {
code_block_regex: Regex::new(r"```([a-zA-Z0-9]*)\n([\s\S]*?)\n```").unwrap(),
header_regex: Regex::new(r"^(#{1,6})\s+(.+)$").unwrap(),
link_regex: Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap(),
image_regex: Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap(),
table_regex: Regex::new(r"^\|.*\|$").unwrap(),
list_regex: Regex::new(r"^[\s]*[-*+]\s+(.+)$").unwrap(),
quote_regex: Regex::new(r"^>\s+(.+)$").unwrap(),
}
}
pub fn parse_markdown(&self, content: &str) -> Result<Vec<MarkdownSection>> {
let mut sections = Vec::new();
let mut current_text = String::new();
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
// Check for code blocks
if line.starts_with("```") {
// Save accumulated text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
current_text.clear();
}
// Parse code block
let (code_section, lines_consumed) = self.parse_code_block(&lines[i..])?;
sections.push(code_section);
i += lines_consumed;
continue;
}
// Check for headers
if let Some(caps) = self.header_regex.captures(line) {
// Save accumulated text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
current_text.clear();
}
let level = caps.get(1).unwrap().as_str().len() as u8;
let header_text = caps.get(2).unwrap().as_str().to_string();
sections.push(MarkdownSection::Header(header_text, level));
i += 1;
continue;
}
// Check for tables
if self.table_regex.is_match(line) {
// Save accumulated text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
current_text.clear();
}
let (table_section, lines_consumed) = self.parse_table(&lines[i..])?;
sections.push(table_section);
i += lines_consumed;
continue;
}
// Check for quotes
if let Some(caps) = self.quote_regex.captures(line) {
// Save accumulated text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
current_text.clear();
}
let quote_text = caps.get(1).unwrap().as_str().to_string();
sections.push(MarkdownSection::Quote(quote_text));
i += 1;
continue;
}
// Check for lists
if let Some(caps) = self.list_regex.captures(line) {
// Save accumulated text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
current_text.clear();
}
let list_text = caps.get(1).unwrap().as_str().to_string();
sections.push(MarkdownSection::List(list_text));
i += 1;
continue;
}
// Accumulate regular text
current_text.push_str(line);
current_text.push('\n');
i += 1;
}
// Process remaining text
if !current_text.trim().is_empty() {
sections.extend(self.parse_text_sections(&current_text)?);
}
Ok(sections)
}
fn parse_code_block(&self, lines: &[&str]) -> Result<(MarkdownSection, usize)> {
if lines.is_empty() || !lines[0].starts_with("```") {
anyhow::bail!("Not a code block");
}
let first_line = lines[0];
let language = if first_line.len() > 3 {
Some(first_line[3..].trim().to_string())
} else {
None
};
let mut content = String::new();
let mut end_index = 1;
for (i, &line) in lines[1..].iter().enumerate() {
if line.starts_with("```") {
end_index = i + 2; // +1 for slice offset, +1 for closing line
break;
}
if i > 0 {
content.push('\n');
}
content.push_str(line);
}
Ok((MarkdownSection::Code(content, language), end_index))
}
fn parse_table(&self, lines: &[&str]) -> Result<(MarkdownSection, usize)> {
let mut table_content = String::new();
let mut line_count = 0;
for &line in lines {
if self.table_regex.is_match(line) {
if line_count > 0 {
table_content.push('\n');
}
table_content.push_str(line);
line_count += 1;
} else {
break;
}
}
Ok((MarkdownSection::Table(table_content), line_count))
}
fn parse_text_sections(&self, text: &str) -> Result<Vec<MarkdownSection>> {
let mut sections = Vec::new();
let mut remaining = text;
// Look for images first (they should be preserved)
while let Some(caps) = self.image_regex.captures(remaining) {
let full_match = caps.get(0).unwrap();
let before = &remaining[..full_match.start()];
let alt = caps.get(1).unwrap().as_str().to_string();
let url = caps.get(2).unwrap().as_str().to_string();
if !before.trim().is_empty() {
sections.push(MarkdownSection::Text(before.to_string()));
}
sections.push(MarkdownSection::Image(alt, url));
remaining = &remaining[full_match.end()..];
}
// Look for links
let mut current_text = remaining.to_string();
while let Some(caps) = self.link_regex.captures(&current_text) {
let full_match = caps.get(0).unwrap();
let before = &current_text[..full_match.start()];
let link_text = caps.get(1).unwrap().as_str().to_string();
let url = caps.get(2).unwrap().as_str().to_string();
if !before.trim().is_empty() {
sections.push(MarkdownSection::Text(before.to_string()));
}
sections.push(MarkdownSection::Link(link_text, url));
current_text = current_text[full_match.end()..].to_string();
}
// Add remaining text
if !current_text.trim().is_empty() {
sections.push(MarkdownSection::Text(current_text));
}
Ok(sections)
}
pub fn rebuild_markdown(&self, sections: Vec<MarkdownSection>) -> String {
let mut result = String::new();
for section in sections {
match section {
MarkdownSection::Text(text) => {
result.push_str(&text);
}
MarkdownSection::Code(content, Some(lang)) => {
result.push_str(&format!("```{}\n{}\n```\n", lang, content));
}
MarkdownSection::Code(content, None) => {
result.push_str(&format!("```\n{}\n```\n", content));
}
MarkdownSection::Header(text, level) => {
let hashes = "#".repeat(level as usize);
result.push_str(&format!("{} {}\n", hashes, text));
}
MarkdownSection::Link(text, url) => {
result.push_str(&format!("[{}]({})", text, url));
}
MarkdownSection::Image(alt, url) => {
result.push_str(&format!("![{}]({})", alt, url));
}
MarkdownSection::Table(content) => {
result.push_str(&content);
result.push('\n');
}
MarkdownSection::List(text) => {
result.push_str(&format!("- {}\n", text));
}
MarkdownSection::Quote(text) => {
result.push_str(&format!("> {}\n", text));
}
}
}
result
}
}

123
src/translator/mod.rs Normal file
View File

@ -0,0 +1,123 @@
pub mod ollama_translator;
pub mod markdown_parser;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranslationConfig {
pub source_lang: String,
pub target_lang: String,
pub ollama_endpoint: String,
pub model: String,
pub preserve_code: bool,
pub preserve_links: bool,
}
impl Default for TranslationConfig {
fn default() -> Self {
Self {
source_lang: "ja".to_string(),
target_lang: "en".to_string(),
ollama_endpoint: "http://localhost:11434".to_string(),
model: "qwen2.5:latest".to_string(),
preserve_code: true,
preserve_links: true,
}
}
}
#[derive(Debug, Clone)]
pub enum MarkdownSection {
Text(String),
Code(String, Option<String>), // content, language
Header(String, u8), // content, level (1-6)
Link(String, String), // text, url
Image(String, String), // alt, url
Table(String),
List(String),
Quote(String),
}
pub trait Translator {
async fn translate(&self, content: &str, config: &TranslationConfig) -> Result<String>;
async fn translate_markdown(&self, content: &str, config: &TranslationConfig) -> Result<String>;
async fn translate_sections(&self, sections: Vec<MarkdownSection>, config: &TranslationConfig) -> Result<Vec<MarkdownSection>>;
}
pub struct TranslationResult {
pub original: String,
pub translated: String,
pub source_lang: String,
pub target_lang: String,
pub model: String,
pub metrics: TranslationMetrics,
}
#[derive(Debug, Clone, Default)]
pub struct TranslationMetrics {
pub character_count: usize,
pub word_count: usize,
pub translation_time_ms: u64,
pub sections_translated: usize,
pub sections_preserved: usize,
}
pub struct LanguageMapping {
pub mappings: HashMap<String, LanguageInfo>,
}
#[derive(Debug, Clone)]
pub struct LanguageInfo {
pub name: String,
pub code: String,
pub ollama_prompt: String,
}
impl LanguageMapping {
pub fn new() -> Self {
let mut mappings = HashMap::new();
// 主要言語の設定
mappings.insert("ja".to_string(), LanguageInfo {
name: "Japanese".to_string(),
code: "ja".to_string(),
ollama_prompt: "You are a professional Japanese translator specializing in technical documentation.".to_string(),
});
mappings.insert("en".to_string(), LanguageInfo {
name: "English".to_string(),
code: "en".to_string(),
ollama_prompt: "You are a professional English translator specializing in technical documentation.".to_string(),
});
mappings.insert("zh".to_string(), LanguageInfo {
name: "Chinese".to_string(),
code: "zh".to_string(),
ollama_prompt: "You are a professional Chinese translator specializing in technical documentation.".to_string(),
});
mappings.insert("ko".to_string(), LanguageInfo {
name: "Korean".to_string(),
code: "ko".to_string(),
ollama_prompt: "You are a professional Korean translator specializing in technical documentation.".to_string(),
});
mappings.insert("es".to_string(), LanguageInfo {
name: "Spanish".to_string(),
code: "es".to_string(),
ollama_prompt: "You are a professional Spanish translator specializing in technical documentation.".to_string(),
});
Self { mappings }
}
pub fn get_language_info(&self, code: &str) -> Option<&LanguageInfo> {
self.mappings.get(code)
}
pub fn get_supported_languages(&self) -> Vec<String> {
self.mappings.keys().cloned().collect()
}
}

View File

@ -0,0 +1,214 @@
use anyhow::Result;
use reqwest::Client;
use serde_json::json;
use std::time::Instant;
use super::*;
use crate::translator::markdown_parser::MarkdownParser;
pub struct OllamaTranslator {
client: Client,
language_mapping: LanguageMapping,
parser: MarkdownParser,
}
impl OllamaTranslator {
pub fn new() -> Self {
Self {
client: Client::new(),
language_mapping: LanguageMapping::new(),
parser: MarkdownParser::new(),
}
}
async fn call_ollama(&self, prompt: &str, config: &TranslationConfig) -> Result<String> {
let request_body = json!({
"model": config.model,
"prompt": prompt,
"stream": false,
"options": {
"temperature": 0.3,
"top_p": 0.9,
"top_k": 40
}
});
let url = format!("{}/api/generate", config.ollama_endpoint);
let response = self.client
.post(&url)
.json(&request_body)
.send()
.await?;
if !response.status().is_success() {
anyhow::bail!("Ollama API request failed: {}", response.status());
}
let response_text = response.text().await?;
let response_json: serde_json::Value = serde_json::from_str(&response_text)?;
let translated = response_json
.get("response")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid response from Ollama"))?;
Ok(translated.to_string())
}
fn build_translation_prompt(&self, text: &str, config: &TranslationConfig) -> Result<String> {
let source_info = self.language_mapping.get_language_info(&config.source_lang)
.ok_or_else(|| anyhow::anyhow!("Unsupported source language: {}", config.source_lang))?;
let target_info = self.language_mapping.get_language_info(&config.target_lang)
.ok_or_else(|| anyhow::anyhow!("Unsupported target language: {}", config.target_lang))?;
let prompt = format!(
r#"{system_prompt}
Translate the following text from {source_lang} to {target_lang}.
IMPORTANT RULES:
1. Preserve all Markdown formatting (headers, links, code blocks, etc.)
2. Do NOT translate content within code blocks (```)
3. Do NOT translate URLs or file paths
4. Preserve technical terms when appropriate
5. Maintain the original structure and formatting
6. Only output the translated text, no explanations
Original text ({source_code}):
{text}
Translated text ({target_code}):"#,
system_prompt = target_info.ollama_prompt,
source_lang = source_info.name,
target_lang = target_info.name,
source_code = source_info.code,
target_code = target_info.code,
text = text
);
Ok(prompt)
}
fn build_section_translation_prompt(&self, section: &MarkdownSection, config: &TranslationConfig) -> Result<String> {
let target_info = self.language_mapping.get_language_info(&config.target_lang)
.ok_or_else(|| anyhow::anyhow!("Unsupported target language: {}", config.target_lang))?;
let (content, section_type) = match section {
MarkdownSection::Text(text) => (text.clone(), "text"),
MarkdownSection::Header(text, _) => (text.clone(), "header"),
MarkdownSection::Quote(text) => (text.clone(), "quote"),
MarkdownSection::List(text) => (text.clone(), "list"),
_ => return Ok(String::new()), // Skip translation for code, links, etc.
};
let prompt = format!(
r#"{system_prompt}
Translate this {section_type} from {source_lang} to {target_lang}.
RULES:
- Only translate the text content
- Preserve formatting symbols (*, #, >, etc.)
- Keep technical terms when appropriate
- Output only the translated text
Text to translate:
{content}
Translation:"#,
system_prompt = target_info.ollama_prompt,
section_type = section_type,
source_lang = config.source_lang,
target_lang = config.target_lang,
content = content
);
Ok(prompt)
}
}
impl Translator for OllamaTranslator {
async fn translate(&self, content: &str, config: &TranslationConfig) -> Result<String> {
let prompt = self.build_translation_prompt(content, config)?;
self.call_ollama(&prompt, config).await
}
async fn translate_markdown(&self, content: &str, config: &TranslationConfig) -> Result<String> {
println!("🔄 Parsing markdown content...");
let sections = self.parser.parse_markdown(content)?;
println!("📝 Found {} sections to process", sections.len());
let translated_sections = self.translate_sections(sections, config).await?;
println!("✅ Rebuilding markdown from translated sections...");
let result = self.parser.rebuild_markdown(translated_sections);
Ok(result)
}
async fn translate_sections(&self, sections: Vec<MarkdownSection>, config: &TranslationConfig) -> Result<Vec<MarkdownSection>> {
let mut translated_sections = Vec::new();
let start_time = Instant::now();
for (index, section) in sections.into_iter().enumerate() {
println!(" 🔤 Processing section {}", index + 1);
let translated_section = match &section {
MarkdownSection::Code(content, lang) => {
if config.preserve_code {
println!(" ⏭️ Preserving code block");
section // Preserve code blocks
} else {
section // Still preserve for now
}
}
MarkdownSection::Link(text, url) => {
if config.preserve_links {
println!(" ⏭️ Preserving link");
section // Preserve links
} else {
// Translate link text only
let prompt = self.build_section_translation_prompt(&MarkdownSection::Text(text.clone()), config)?;
let translated_text = self.call_ollama(&prompt, config).await?;
MarkdownSection::Link(translated_text.trim().to_string(), url.clone())
}
}
MarkdownSection::Image(alt, url) => {
println!(" 🖼️ Preserving image");
section // Preserve images
}
MarkdownSection::Table(content) => {
println!(" 📊 Translating table content");
let prompt = self.build_section_translation_prompt(&MarkdownSection::Text(content.clone()), config)?;
let translated_content = self.call_ollama(&prompt, config).await?;
MarkdownSection::Table(translated_content.trim().to_string())
}
_ => {
// Translate text sections
println!(" 🔤 Translating text");
let prompt = self.build_section_translation_prompt(&section, config)?;
let translated_text = self.call_ollama(&prompt, config).await?;
match section {
MarkdownSection::Text(_) => MarkdownSection::Text(translated_text.trim().to_string()),
MarkdownSection::Header(_, level) => MarkdownSection::Header(translated_text.trim().to_string(), level),
MarkdownSection::Quote(_) => MarkdownSection::Quote(translated_text.trim().to_string()),
MarkdownSection::List(_) => MarkdownSection::List(translated_text.trim().to_string()),
_ => section,
}
}
};
translated_sections.push(translated_section);
// Add small delay to avoid overwhelming Ollama
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
let elapsed = start_time.elapsed();
println!("⏱️ Translation completed in {:.2}s", elapsed.as_secs_f64());
Ok(translated_sections)
}
}

103
templates/api.md Normal file
View File

@ -0,0 +1,103 @@
# API Documentation
## Public Functions
{{#each api.public_functions}}
### `{{this.name}}`
{{#if this.docs}}
{{this.docs}}
{{/if}}
**Visibility:** `{{this.visibility}}`
{{#if this.is_async}}**Async:** Yes{{/if}}
{{#if this.parameters}}
**Parameters:**
{{#each this.parameters}}
- `{{this.name}}`: `{{this.param_type}}`{{#if this.is_mutable}} (mutable){{/if}}
{{/each}}
{{/if}}
{{#if this.return_type}}
**Returns:** `{{this.return_type}}`
{{/if}}
---
{{/each}}
## Public Structs
{{#each api.public_structs}}
### `{{this.name}}`
{{#if this.docs}}
{{this.docs}}
{{/if}}
**Visibility:** `{{this.visibility}}`
{{#if this.fields}}
**Fields:**
{{#each this.fields}}
- `{{this.name}}`: `{{this.field_type}}` ({{this.visibility}})
{{#if this.docs}} - {{this.docs}}{{/if}}
{{/each}}
{{/if}}
---
{{/each}}
## Public Enums
{{#each api.public_enums}}
### `{{this.name}}`
{{#if this.docs}}
{{this.docs}}
{{/if}}
**Visibility:** `{{this.visibility}}`
{{#if this.variants}}
**Variants:**
{{#each this.variants}}
- `{{this.name}}`
{{#if this.docs}} - {{this.docs}}{{/if}}
{{#if this.fields}}
**Fields:**
{{#each this.fields}}
- `{{this.name}}`: `{{this.field_type}}`
{{/each}}
{{/if}}
{{/each}}
{{/if}}
---
{{/each}}
## Public Traits
{{#each api.public_traits}}
### `{{this.name}}`
{{#if this.docs}}
{{this.docs}}
{{/if}}
**Visibility:** `{{this.visibility}}`
{{#if this.methods}}
**Methods:**
{{#each this.methods}}
- `{{this.name}}({{#each this.parameters}}{{this.name}}: {{this.param_type}}{{#unless @last}}, {{/unless}}{{/each}}){{#if this.return_type}} -> {{this.return_type}}{{/if}}`
{{#if this.docs}} - {{this.docs}}{{/if}}
{{/each}}
{{/if}}
---
{{/each}}

19
templates/changelog.md Normal file
View File

@ -0,0 +1,19 @@
# Changelog
## Recent Changes
{{#each commits}}
### {{this.date}}
**{{this.hash}}** by {{this.author}}
{{this.message}}
---
{{/each}}
## Summary
- **Total Commits:** {{commits.length}}
- **Contributors:** {{#unique commits "author"}}{{this.author}}{{#unless @last}}, {{/unless}}{{/unique}}

76
templates/readme.md Normal file
View File

@ -0,0 +1,76 @@
# {{project.name}}
{{#if project.description}}
{{project.description}}
{{/if}}
## Overview
This project contains {{project.modules.length}} modules with a total of {{project.metrics.total_lines}} lines of code.
## Installation
```bash
cargo install {{project.name}}
```
## Usage
```bash
{{project.name}} --help
```
## Dependencies
{{#each project.dependencies}}
- `{{@key}}`: {{this}}
{{/each}}
## Project Structure
```
{{#each project.structure.directories}}
{{this.name}}/
{{/each}}
```
## API Documentation
{{#each project.modules}}
### {{this.name}}
{{#if this.docs}}
{{this.docs}}
{{/if}}
{{#if this.functions}}
**Functions:** {{this.functions.length}}
{{/if}}
{{#if this.structs}}
**Structs:** {{this.structs.length}}
{{/if}}
{{/each}}
## Metrics
- **Lines of Code:** {{project.metrics.total_lines}}
- **Total Files:** {{project.metrics.total_files}}
- **Test Files:** {{project.metrics.test_files}}
- **Dependencies:** {{project.metrics.dependency_count}}
- **Complexity Score:** {{project.metrics.complexity_score}}
## License
{{#if project.license}}
{{project.license}}
{{else}}
MIT
{{/if}}
## Authors
{{#each project.authors}}
- {{this}}
{{/each}}

39
templates/structure.md Normal file
View File

@ -0,0 +1,39 @@
# Project Structure
## Directory Overview
```
{{#each structure.directories}}
{{this.name}}/
{{#each this.subdirectories}}
├── {{this}}/
{{/each}}
{{#if this.file_count}}
└── ({{this.file_count}} files)
{{/if}}
{{/each}}
```
## File Distribution
{{#each structure.files}}
- **{{this.name}}** ({{this.language}}) - {{this.lines_of_code}} lines{{#if this.is_test}} [TEST]{{/if}}
{{/each}}
## Statistics
- **Total Directories:** {{structure.directories.length}}
- **Total Files:** {{structure.files.length}}
- **Languages Used:**
{{#group structure.files by="language"}}
- {{@key}}: {{this.length}} files
{{/group}}
{{#if structure.dependency_graph}}
## Dependencies
{{#each structure.dependency_graph}}
- **{{@key}}** depends on: {{#each this}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
{{/each}}
{{/if}}