From 5783f40de795c4a80ce27fb7a57a8c39d2dcd85a Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 18 Jul 2025 23:06:37 +0800 Subject: [PATCH] :sparkles: added automatic language detection and API --- README.md | 49 +- examples/08-auto-language-detection/README.md | 223 +++++++++ examples/08-auto-language-detection/main.go | 429 ++++++++++++++++++ freeze.go | 81 ++++ generator.go | 61 ++- language_detector.go | 273 +++++++++++ language_detector_test.go | 243 ++++++++++ quickfreeze.go | 58 +++ 8 files changed, 1400 insertions(+), 17 deletions(-) create mode 100644 examples/08-auto-language-detection/README.md create mode 100644 examples/08-auto-language-detection/main.go create mode 100644 language_detector.go create mode 100644 language_detector_test.go diff --git a/README.md b/README.md index a35c730..9d405e8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ FreezeLib is a Go library for generating beautiful screenshots of code and termi ## Features - ๐ŸŽจ **Syntax Highlighting**: Support for 100+ programming languages +- ๐Ÿ” **Auto Language Detection**: Intelligent language detection from code content and filenames - ๐Ÿ–ผ๏ธ **Multiple Output Formats**: Generate SVG and PNG images - ๐ŸŽญ **Rich Themes**: Built-in themes including GitHub, Dracula, Monokai, and more - ๐ŸชŸ **Window Controls**: macOS-style window decorations @@ -60,6 +61,21 @@ func main() { } ``` +### Auto Language Detection + +FreezeLib can automatically detect the programming language: + +```go +freeze := freezelib.New() + +// Automatic language detection from code content +svgData, err := freeze.GenerateFromCodeAuto(code) + +// Detect language manually +language := freeze.DetectLanguage(code) +fmt.Printf("Detected language: %s", language) +``` + ### QuickFreeze API For a more fluent experience, use the QuickFreeze API: @@ -72,7 +88,7 @@ svgData, err := qf.WithTheme("dracula"). WithWindow(). WithShadow(). WithLineNumbers(). - CodeToSVG(code) + CodeToSVGAuto(code) // Auto-detect language ``` ## API Reference @@ -104,6 +120,10 @@ qf := freezelib.NewQuickFreezeWithPreset("terminal") ```go svgData, err := freeze.GenerateFromCode(code, "python") pngData, err := freeze.GeneratePNGFromCode(code, "python") + +// With automatic language detection +svgData, err := freeze.GenerateFromCodeAuto(code) +pngData, err := freeze.GeneratePNGFromCodeAuto(code) ``` #### From File @@ -170,6 +190,33 @@ presets := []string{ freeze := freezelib.NewWithPreset("dark") ``` +### Language Detection + +FreezeLib provides powerful language detection capabilities: + +```go +freeze := freezelib.New() + +// Detect language from code content +language := freeze.DetectLanguage(code) + +// Detect from filename +language = freeze.DetectLanguageFromFilename("script.py") + +// Combined detection (filename + content) +language = freeze.DetectLanguageFromFile("script.py", code) + +// Check language support +supported := freeze.IsLanguageSupported("go") + +// Get all supported languages +languages := freeze.GetSupportedLanguages() + +// Custom language detector +detector := freeze.GetLanguageDetector() +detector.AddCustomMapping(".myext", "python") +``` + ### Chainable Methods Both `Freeze` and `QuickFreeze` support method chaining: diff --git a/examples/08-auto-language-detection/README.md b/examples/08-auto-language-detection/README.md new file mode 100644 index 0000000..3cd8286 --- /dev/null +++ b/examples/08-auto-language-detection/README.md @@ -0,0 +1,223 @@ +# Auto Language Detection Examples + +This example demonstrates FreezeLib's enhanced automatic language detection capabilities. + +## Features + +### ๐ŸŽฏ Automatic Language Detection +- **Content-based detection**: Analyzes code content to identify the programming language +- **Filename-based detection**: Uses file extensions and names to determine language +- **Combined detection**: Intelligently combines both methods for best results +- **Fallback support**: Gracefully handles unknown languages + +### ๐Ÿ”ง Enhanced API +- `GenerateFromCodeAuto()` - Generate screenshots without specifying language +- `DetectLanguage()` - Detect language from code content +- `DetectLanguageFromFilename()` - Detect language from filename +- `DetectLanguageFromFile()` - Combined detection from both filename and content +- `GetSupportedLanguages()` - List all supported languages +- `IsLanguageSupported()` - Check if a language is supported + +### โš™๏ธ Customizable Detection +- Custom file extension mappings +- Configurable detection strategies +- Fallback language settings + +## Usage Examples + +### Basic Auto Detection + +```go +freeze := freezelib.New() + +code := ` +def hello_world(): + print("Hello, World!") + +if __name__ == "__main__": + hello_world() +` + +// Automatically detect language and generate screenshot +svgData, err := freeze.GenerateFromCodeAuto(code) +``` + +### Language Detection API + +```go +freeze := freezelib.New() + +// Detect language from content +language := freeze.DetectLanguage(code) +fmt.Printf("Detected language: %s\n", language) + +// Detect from filename +language = freeze.DetectLanguageFromFilename("script.py") +fmt.Printf("Language from filename: %s\n", language) + +// Combined detection +language = freeze.DetectLanguageFromFile("script.py", code) +fmt.Printf("Combined detection: %s\n", language) +``` + +### Custom Language Detector + +```go +freeze := freezelib.New() + +// Get and customize the language detector +detector := freeze.GetLanguageDetector() + +// Add custom file extension mappings +detector.AddCustomMapping(".myext", "python") +detector.AddCustomMapping(".config", "json") + +// Use custom mappings +language := freeze.DetectLanguageFromFilename("app.config") +// Returns "json" due to custom mapping +``` + +### QuickFreeze Auto Detection + +```go +qf := freezelib.NewQuickFreeze() + +svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + CodeToSVGAuto(code) // Auto-detect language +``` + +## Supported Languages + +FreezeLib supports 100+ programming languages including: + +### Popular Languages +- **Go** - `.go` +- **Python** - `.py`, `.pyw` +- **JavaScript** - `.js`, `.mjs` +- **TypeScript** - `.ts`, `.tsx` +- **Rust** - `.rs` +- **Java** - `.java` +- **C/C++** - `.c`, `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp` +- **C#** - `.cs` +- **PHP** - `.php` +- **Ruby** - `.rb` + +### Web Technologies +- **HTML** - `.html`, `.htm` +- **CSS** - `.css` +- **SCSS/Sass** - `.scss`, `.sass` +- **JSON** - `.json` +- **XML** - `.xml` + +### Shell & Scripts +- **Bash** - `.sh`, `.bash` +- **PowerShell** - `.ps1` +- **Batch** - `.bat`, `.cmd` +- **Fish** - `.fish` +- **Zsh** - `.zsh` + +### Configuration & Data +- **YAML** - `.yaml`, `.yml` +- **TOML** - `.toml` +- **INI** - `.ini`, `.cfg`, `.conf` +- **SQL** - `.sql` +- **Dockerfile** - `Dockerfile`, `.dockerfile` + +### And Many More... +- Kotlin, Swift, Scala, Clojure, Haskell, OCaml, F#, Erlang, Elixir +- Julia, Nim, Zig, V, D, Pascal, Ada, Fortran, COBOL +- Assembly (NASM, GAS), MATLAB, R, Lua, Dart, Elm +- GraphQL, Protocol Buffers, Markdown, LaTeX, Vim script + +## Detection Strategies + +### 1. Content Analysis +Uses Chroma's built-in lexer analysis to examine code patterns, keywords, and syntax. + +```go +// Analyzes code structure and syntax +language := freeze.DetectLanguage(` +package main +import "fmt" +func main() { fmt.Println("Hello") } +`) +// Returns: "go" +``` + +### 2. Filename Analysis +Examines file extensions and special filenames. + +```go +// Uses file extension mapping +language := freeze.DetectLanguageFromFilename("script.py") +// Returns: "python" + +// Recognizes special files +language := freeze.DetectLanguageFromFilename("Dockerfile") +// Returns: "dockerfile" +``` + +### 3. Combined Analysis +Intelligently combines both methods for best accuracy. + +```go +// Tries filename first, then content analysis +language := freeze.DetectLanguageFromFile("script.unknown", pythonCode) +// Returns: "python" (from content analysis) +``` + +## Configuration Options + +### Language Detector Settings + +```go +detector := freeze.GetLanguageDetector() + +// Enable/disable detection methods +detector.EnableContentAnalysis = true +detector.EnableFilenameAnalysis = true + +// Set fallback language +detector.FallbackLanguage = "text" + +// Add custom mappings +detector.AddCustomMapping(".myext", "python") +``` + +## Error Handling + +```go +svgData, err := freeze.GenerateFromCodeAuto(code) +if err != nil { + if strings.Contains(err.Error(), "could not determine language") { + // Language detection failed + // Try with explicit language or check supported languages + fmt.Println("Supported languages:", freeze.GetSupportedLanguages()) + } +} +``` + +## Running the Examples + +```bash +cd examples/08-auto-language-detection +go run main.go +``` + +This will generate various screenshots demonstrating: +- Basic auto detection with different languages +- Language detection API usage +- Custom language detector configuration +- Batch processing with auto detection +- Language analysis and support information + +## Output Files + +The examples generate several SVG files in the `output/` directory: +- `auto_*.svg` - Basic auto detection examples +- `detection_*.svg` - Language detection API examples +- `custom_*.svg` - Custom detector examples +- `batch_*.svg` - Batch processing examples +- `language_summary.svg` - Language support summary diff --git a/examples/08-auto-language-detection/main.go b/examples/08-auto-language-detection/main.go new file mode 100644 index 0000000..6167fdd --- /dev/null +++ b/examples/08-auto-language-detection/main.go @@ -0,0 +1,429 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("๐Ÿ” FreezeLib Auto Language Detection Examples") + fmt.Println("=============================================") + + // Create output directory + err := os.MkdirAll("output", 0755) + if err != nil { + fmt.Printf("โŒ Error creating output directory: %v\n", err) + return + } + + // Run examples + basicAutoDetectionExample() + languageDetectionAPIExample() + customLanguageDetectorExample() + batchAutoDetectionExample() + languageAnalysisExample() +} + +// Basic auto detection example +func basicAutoDetectionExample() { + fmt.Println("\n๐ŸŽฏ Basic Auto Detection") + fmt.Println("-----------------------") + + freeze := freezelib.New() + + // Different code samples without specifying language + codeSamples := []struct { + name string + code string + }{ + { + "go_example", + `package main + +import "fmt" + +func main() { + fmt.Println("Hello, Go!") + + // Goroutine example + go func() { + fmt.Println("Running in goroutine") + }() +}`, + }, + { + "python_example", + `#!/usr/bin/env python3 + +def fibonacci(n): + """Calculate fibonacci number recursively.""" + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +if __name__ == "__main__": + print(f"Fibonacci(10) = {fibonacci(10)}")`, + }, + { + "javascript_example", + `// Modern JavaScript with async/await +async function fetchUserData(userId) { + try { + const response = await fetch('/api/users/' + userId); + const userData = await response.json(); + + return { + ...userData, + lastUpdated: new Date().toISOString() + }; + } catch (error) { + console.error('Failed to fetch user data:', error); + throw error; + } +}`, + }, + { + "rust_example", + `use std::collections::HashMap; + +fn main() { + let mut scores = HashMap::new(); + + scores.insert(String::from("Blue"), 10); + scores.insert(String::from("Yellow"), 50); + + for (key, value) in &scores { + println!("{}: {}", key, value); + } +}`, + }, + } + + for _, sample := range codeSamples { + fmt.Printf("๐Ÿ” Processing %s (auto-detecting language)...\n", sample.name) + + // Detect language first + detectedLang := freeze.DetectLanguage(sample.code) + fmt.Printf(" Detected language: %s\n", detectedLang) + + // Generate with auto detection + svgData, err := freeze.GenerateFromCodeAuto(sample.code) + if err != nil { + fmt.Printf("โŒ Error generating %s: %v\n", sample.name, err) + continue + } + + filename := fmt.Sprintf("output/auto_%s.svg", sample.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("โŒ Error saving %s: %v\n", filename, err) + continue + } + + fmt.Printf("โœ… Generated: %s\n", filename) + } +} + +// Language detection API example +func languageDetectionAPIExample() { + fmt.Println("\n๐Ÿ”ง Language Detection API") + fmt.Println("-------------------------") + + freeze := freezelib.New() + + // Test different detection methods + testCodes := []struct { + name string + code string + filename string + }{ + { + "config_file", + `{ + "name": "my-app", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.0", + "lodash": "^4.17.21" + } +}`, + "package.json", + }, + { + "shell_script", + `#!/bin/bash + +# Deploy script +set -e + +echo "Starting deployment..." + +if [ ! -d "dist" ]; then + echo "Building project..." + npm run build +fi + +echo "Deploying to server..." +rsync -av dist/ user@server:/var/www/html/ + +echo "Deployment complete!"`, + "deploy.sh", + }, + { + "dockerfile", + `FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "start"]`, + "Dockerfile", + }, + } + + for _, test := range testCodes { + fmt.Printf("๐Ÿ” Analyzing %s...\n", test.name) + + // Test different detection methods + langFromContent := freeze.DetectLanguage(test.code) + langFromFilename := freeze.DetectLanguageFromFilename(test.filename) + langFromBoth := freeze.DetectLanguageFromFile(test.filename, test.code) + + fmt.Printf(" Content-based: %s\n", langFromContent) + fmt.Printf(" Filename-based: %s\n", langFromFilename) + fmt.Printf(" Combined: %s\n", langFromBoth) + + // Generate screenshot using the best detection + svgData, err := freeze.GenerateFromCodeAuto(test.code) + if err != nil { + fmt.Printf("โŒ Error generating %s: %v\n", test.name, err) + continue + } + + filename := fmt.Sprintf("output/detection_%s.svg", test.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("โŒ Error saving %s: %v\n", filename, err) + continue + } + + fmt.Printf("โœ… Generated: %s\n", filename) + } +} + +// Custom language detector example +func customLanguageDetectorExample() { + fmt.Println("\nโš™๏ธ Custom Language Detector") + fmt.Println("---------------------------") + + freeze := freezelib.New() + + // Get the language detector and customize it + detector := freeze.GetLanguageDetector() + + // Add custom mappings + detector.AddCustomMapping(".myext", "python") + detector.AddCustomMapping(".config", "json") + + // Test custom mappings + customTests := []struct { + filename string + content string + }{ + { + "script.myext", + `def custom_function(): + print("This is a custom extension file") + return True`, + }, + { + "app.config", + `{ + "database": { + "host": "localhost", + "port": 5432 + } +}`, + }, + } + + for _, test := range customTests { + fmt.Printf("๐Ÿ” Testing custom mapping for %s...\n", test.filename) + + detectedLang := freeze.DetectLanguageFromFile(test.filename, test.content) + fmt.Printf(" Detected language: %s\n", detectedLang) + + svgData, err := freeze.GenerateFromCodeAuto(test.content) + if err != nil { + fmt.Printf("โŒ Error generating screenshot: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/custom_%s.svg", filepath.Base(test.filename)) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("โŒ Error saving %s: %v\n", filename, err) + continue + } + + fmt.Printf("โœ… Generated: %s\n", filename) + } +} + +// Batch auto detection example +func batchAutoDetectionExample() { + fmt.Println("\n๐Ÿ“ฆ Batch Auto Detection") + fmt.Println("-----------------------") + + // Create sample files with different languages + sampleFiles := map[string]string{ + "hello.go": `package main + +import "fmt" + +func main() { + fmt.Println("Hello from Go!") +}`, + "hello.py": `def main(): + print("Hello from Python!") + +if __name__ == "__main__": + main()`, + "hello.js": `function main() { + console.log("Hello from JavaScript!"); +} + +main();`, + "hello.rs": `fn main() { + println!("Hello from Rust!"); +}`, + "style.css": `body { + font-family: 'Arial', sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 20px; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +}`, + } + + // Create temporary files + tempDir := "temp_files" + err := os.MkdirAll(tempDir, 0755) + if err != nil { + fmt.Printf("โŒ Error creating temp directory: %v\n", err) + return + } + defer os.RemoveAll(tempDir) + + // Write sample files + for filename, content := range sampleFiles { + filePath := filepath.Join(tempDir, filename) + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + fmt.Printf("โŒ Error writing %s: %v\n", filename, err) + continue + } + } + + freeze := freezelib.New() + + // Process each file with auto detection + for filename := range sampleFiles { + filePath := filepath.Join(tempDir, filename) + fmt.Printf("๐Ÿ“„ Processing %s...\n", filename) + + svgData, err := freeze.GenerateFromFile(filePath) + if err != nil { + fmt.Printf("โŒ Error processing %s: %v\n", filename, err) + continue + } + + outputFile := fmt.Sprintf("output/batch_%s.svg", filename) + err = os.WriteFile(outputFile, svgData, 0644) + if err != nil { + fmt.Printf("โŒ Error saving %s: %v\n", outputFile, err) + continue + } + + fmt.Printf("โœ… Generated: %s\n", outputFile) + } +} + +// Language analysis example +func languageAnalysisExample() { + fmt.Println("\n๐Ÿ“Š Language Analysis") + fmt.Println("--------------------") + + freeze := freezelib.New() + + // Get supported languages + supportedLangs := freeze.GetSupportedLanguages() + fmt.Printf("๐Ÿ“ˆ Total supported languages: %d\n", len(supportedLangs)) + + // Show first 20 languages + fmt.Println("๐Ÿ”ค First 20 supported languages:") + for i, lang := range supportedLangs { + if i >= 20 { + break + } + fmt.Printf(" %d. %s\n", i+1, lang) + } + + // Test language support + testLanguages := []string{"go", "python", "javascript", "rust", "unknown-lang"} + fmt.Println("\n๐Ÿงช Testing language support:") + for _, lang := range testLanguages { + supported := freeze.IsLanguageSupported(lang) + status := "โŒ" + if supported { + status = "โœ…" + } + fmt.Printf(" %s %s\n", status, lang) + } + + // Create a summary file + summaryContent := fmt.Sprintf(`# FreezeLib Language Support Summary + +Total supported languages: %d + +## Sample of supported languages: +`, len(supportedLangs)) + + for i, lang := range supportedLangs { + if i >= 50 { + summaryContent += "... and more\n" + break + } + summaryContent += fmt.Sprintf("- %s\n", lang) + } + + svgData, err := freeze.GenerateFromCode(summaryContent, "markdown") + if err != nil { + fmt.Printf("โŒ Error generating summary: %v\n", err) + return + } + + err = os.WriteFile("output/language_summary.svg", svgData, 0644) + if err != nil { + fmt.Printf("โŒ Error saving summary: %v\n", err) + return + } + + fmt.Printf("โœ… Generated language summary: output/language_summary.svg\n") +} diff --git a/freeze.go b/freeze.go index aeb9dd2..19fd581 100644 --- a/freeze.go +++ b/freeze.go @@ -71,6 +71,11 @@ func (f *Freeze) GenerateFromCode(code, language string) ([]byte, error) { return f.generator.GenerateFromCode(code, language) } +// GenerateFromCodeAuto generates an SVG screenshot from source code with automatic language detection +func (f *Freeze) GenerateFromCodeAuto(code string) ([]byte, error) { + return f.generator.GenerateFromCode(code, "") +} + // GenerateFromFile generates an SVG screenshot from a source code file func (f *Freeze) GenerateFromFile(filename string) ([]byte, error) { return f.generator.GenerateFromFile(filename) @@ -112,6 +117,28 @@ func (f *Freeze) GeneratePNGFromCode(code, language string) ([]byte, error) { return f.generator.ConvertToPNG(svgData, width, height) } +// GeneratePNGFromCodeAuto generates a PNG screenshot from source code with automatic language detection +func (f *Freeze) GeneratePNGFromCodeAuto(code string) ([]byte, error) { + svgData, err := f.generator.GenerateFromCode(code, "") + if err != nil { + return nil, err + } + + // Calculate dimensions for PNG (use 4x scale for better quality) + width := f.config.Width + height := f.config.Height + if width == 0 || height == 0 { + // Use default dimensions with 4x scale + width = 800 * 4 + height = 600 * 4 + } else { + width *= 4 + height *= 4 + } + + return f.generator.ConvertToPNG(svgData, width, height) +} + // GeneratePNGFromFile generates a PNG screenshot from a source code file func (f *Freeze) GeneratePNGFromFile(filename string) ([]byte, error) { svgData, err := f.generator.GenerateFromFile(filename) @@ -177,6 +204,24 @@ func (f *Freeze) SaveCodeToFile(code, language, filename string) error { return f.SaveToFile(data, filename) } +// SaveCodeToFileAuto generates and saves a code screenshot to a file with automatic language detection +func (f *Freeze) SaveCodeToFileAuto(code, filename string) error { + var data []byte + var err error + + if isPNGFile(filename) { + data, err = f.GeneratePNGFromCodeAuto(code) + } else { + data, err = f.GenerateFromCodeAuto(code) + } + + if err != nil { + return err + } + + return f.SaveToFile(data, filename) +} + // SaveFileToFile generates and saves a file screenshot to a file func (f *Freeze) SaveFileToFile(inputFile, outputFile string) error { var data []byte @@ -298,6 +343,42 @@ func (f *Freeze) WithDimensions(width, height float64) *Freeze { return clone } +// DetectLanguage detects the programming language from code content +func (f *Freeze) DetectLanguage(code string) string { + return f.generator.DetectLanguage(code) +} + +// DetectLanguageFromFilename detects the programming language from filename +func (f *Freeze) DetectLanguageFromFilename(filename string) string { + return f.generator.DetectLanguageFromFilename(filename) +} + +// DetectLanguageFromFile detects language from both filename and content +func (f *Freeze) DetectLanguageFromFile(filename, content string) string { + return f.generator.DetectLanguageFromFile(filename, content) +} + +// GetSupportedLanguages returns a list of all supported languages +func (f *Freeze) GetSupportedLanguages() []string { + return f.generator.GetSupportedLanguages() +} + +// IsLanguageSupported checks if a language is supported +func (f *Freeze) IsLanguageSupported(language string) bool { + return f.generator.IsLanguageSupported(language) +} + +// SetLanguageDetector sets a custom language detector +func (f *Freeze) SetLanguageDetector(detector *LanguageDetector) *Freeze { + f.generator.SetLanguageDetector(detector) + return f +} + +// GetLanguageDetector returns the current language detector +func (f *Freeze) GetLanguageDetector() *LanguageDetector { + return f.generator.GetLanguageDetector() +} + // isPNGFile checks if the filename has a PNG extension func isPNGFile(filename string) bool { return len(filename) > 4 && filename[len(filename)-4:] == ".png" diff --git a/generator.go b/generator.go index b142ea6..36bf4d1 100644 --- a/generator.go +++ b/generator.go @@ -12,7 +12,6 @@ import ( "github.com/alecthomas/chroma/v2" formatter "github.com/alecthomas/chroma/v2/formatters/svg" - "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/beevik/etree" "github.com/charmbracelet/lipgloss" @@ -28,7 +27,8 @@ const ( // Generator handles the core screenshot generation logic type Generator struct { - config *Config + config *Config + languageDetector *LanguageDetector } // NewGenerator creates a new generator with the given configuration @@ -36,7 +36,10 @@ func NewGenerator(config *Config) *Generator { if config == nil { config = DefaultConfig() } - return &Generator{config: config} + return &Generator{ + config: config, + languageDetector: NewLanguageDetector(), + } } // GenerateFromCode generates an SVG from source code @@ -50,14 +53,8 @@ func (g *Generator) GenerateFromCode(code, language string) ([]byte, error) { g.config.Language = language } - // Get lexer for the language - var lexer chroma.Lexer - if g.config.Language != "" { - lexer = lexers.Get(g.config.Language) - } - if lexer == nil { - lexer = lexers.Analyse(code) - } + // Get lexer for the language using enhanced detection + lexer := g.languageDetector.GetLexer(g.config.Language, code) if lexer == nil { return nil, errors.New("could not determine language for syntax highlighting") } @@ -79,11 +76,8 @@ func (g *Generator) GenerateFromFile(filename string) ([]byte, error) { code := string(content) - // Get lexer from filename - lexer := lexers.Get(filename) - if lexer == nil { - lexer = lexers.Analyse(code) - } + // Get lexer from filename and content using enhanced detection + lexer := g.languageDetector.GetLexerFromFile(filename, code) if lexer == nil { return nil, errors.New("could not determine language for syntax highlighting") } @@ -91,6 +85,41 @@ func (g *Generator) GenerateFromFile(filename string) ([]byte, error) { return g.generateSVG(code, lexer, false) } +// DetectLanguage detects the programming language from code content +func (g *Generator) DetectLanguage(code string) string { + return g.languageDetector.DetectLanguage(code) +} + +// DetectLanguageFromFilename detects the programming language from filename +func (g *Generator) DetectLanguageFromFilename(filename string) string { + return g.languageDetector.DetectLanguageFromFilename(filename) +} + +// DetectLanguageFromFile detects language from both filename and content +func (g *Generator) DetectLanguageFromFile(filename, content string) string { + return g.languageDetector.DetectLanguageFromFile(filename, content) +} + +// GetSupportedLanguages returns a list of all supported languages +func (g *Generator) GetSupportedLanguages() []string { + return g.languageDetector.GetSupportedLanguages() +} + +// IsLanguageSupported checks if a language is supported +func (g *Generator) IsLanguageSupported(language string) bool { + return g.languageDetector.IsLanguageSupported(language) +} + +// SetLanguageDetector sets a custom language detector +func (g *Generator) SetLanguageDetector(detector *LanguageDetector) { + g.languageDetector = detector +} + +// GetLanguageDetector returns the current language detector +func (g *Generator) GetLanguageDetector() *LanguageDetector { + return g.languageDetector +} + // GenerateFromANSI generates an SVG from ANSI terminal output func (g *Generator) GenerateFromANSI(ansiOutput string) ([]byte, error) { if err := g.config.Validate(); err != nil { diff --git a/language_detector.go b/language_detector.go new file mode 100644 index 0000000..ebed9bb --- /dev/null +++ b/language_detector.go @@ -0,0 +1,273 @@ +package freezelib + +import ( + "path/filepath" + "strings" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" +) + +// LanguageDetector provides enhanced language detection capabilities +type LanguageDetector struct { + // EnableContentAnalysis enables content-based language detection + EnableContentAnalysis bool + // EnableFilenameAnalysis enables filename-based language detection + EnableFilenameAnalysis bool + // FallbackLanguage is used when detection fails + FallbackLanguage string + // CustomMappings allows custom file extension to language mappings + CustomMappings map[string]string +} + +// NewLanguageDetector creates a new language detector with default settings +func NewLanguageDetector() *LanguageDetector { + return &LanguageDetector{ + EnableContentAnalysis: true, + EnableFilenameAnalysis: true, + FallbackLanguage: "text", + CustomMappings: make(map[string]string), + } +} + +// DetectLanguage detects the programming language from code content +func (ld *LanguageDetector) DetectLanguage(code string) string { + if !ld.EnableContentAnalysis { + return ld.FallbackLanguage + } + + lexer := lexers.Analyse(code) + if lexer != nil { + config := lexer.Config() + if config != nil && len(config.Aliases) > 0 { + return config.Aliases[0] + } + if config != nil && config.Name != "" { + return strings.ToLower(config.Name) + } + } + + return ld.FallbackLanguage +} + +// DetectLanguageFromFilename detects the programming language from filename +func (ld *LanguageDetector) DetectLanguageFromFilename(filename string) string { + if !ld.EnableFilenameAnalysis { + return ld.FallbackLanguage + } + + // Check custom mappings first + ext := strings.ToLower(filepath.Ext(filename)) + if lang, exists := ld.CustomMappings[ext]; exists { + return lang + } + + // Use chroma's built-in filename detection + lexer := lexers.Match(filename) + if lexer != nil { + config := lexer.Config() + if config != nil && len(config.Aliases) > 0 { + // Return the first alias which is usually the most common name + return config.Aliases[0] + } + if config != nil && config.Name != "" { + return strings.ToLower(config.Name) + } + } + + // Fallback to common extension mappings + return ld.detectFromExtension(ext) +} + +// DetectLanguageFromFile detects language from both filename and content +func (ld *LanguageDetector) DetectLanguageFromFile(filename, content string) string { + // Try filename first + if ld.EnableFilenameAnalysis { + lang := ld.DetectLanguageFromFilename(filename) + if lang != ld.FallbackLanguage { + return lang + } + } + + // Try content analysis + if ld.EnableContentAnalysis { + lang := ld.DetectLanguage(content) + if lang != ld.FallbackLanguage { + return lang + } + } + + return ld.FallbackLanguage +} + +// GetLexer returns a chroma lexer for the given language or content +func (ld *LanguageDetector) GetLexer(language, content string) chroma.Lexer { + var lexer chroma.Lexer + + // Try to get lexer by language name + if language != "" { + lexer = lexers.Get(language) + if lexer != nil { + return lexer + } + } + + // Try content analysis if enabled + if ld.EnableContentAnalysis && content != "" { + lexer = lexers.Analyse(content) + if lexer != nil { + return lexer + } + } + + // Return fallback lexer + return lexers.Get(ld.FallbackLanguage) +} + +// GetLexerFromFile returns a chroma lexer for the given file +func (ld *LanguageDetector) GetLexerFromFile(filename, content string) chroma.Lexer { + var lexer chroma.Lexer + + // Try filename detection first if enabled + if ld.EnableFilenameAnalysis { + lexer = lexers.Match(filename) + if lexer != nil { + return lexer + } + } + + // Try content analysis if enabled + if ld.EnableContentAnalysis && content != "" { + lexer = lexers.Analyse(content) + if lexer != nil { + return lexer + } + } + + // Return fallback lexer + return lexers.Get(ld.FallbackLanguage) +} + +// AddCustomMapping adds a custom file extension to language mapping +func (ld *LanguageDetector) AddCustomMapping(extension, language string) { + if ld.CustomMappings == nil { + ld.CustomMappings = make(map[string]string) + } + ld.CustomMappings[strings.ToLower(extension)] = language +} + +// RemoveCustomMapping removes a custom file extension mapping +func (ld *LanguageDetector) RemoveCustomMapping(extension string) { + if ld.CustomMappings != nil { + delete(ld.CustomMappings, strings.ToLower(extension)) + } +} + +// GetSupportedLanguages returns a list of all supported languages +func (ld *LanguageDetector) GetSupportedLanguages() []string { + return lexers.Names(false) // false means don't include aliases +} + +// IsLanguageSupported checks if a language is supported +func (ld *LanguageDetector) IsLanguageSupported(language string) bool { + lexer := lexers.Get(language) + return lexer != nil +} + +// detectFromExtension provides fallback extension-based detection +func (ld *LanguageDetector) detectFromExtension(ext string) string { + commonMappings := map[string]string{ + ".go": "go", + ".py": "python", + ".js": "javascript", + ".ts": "typescript", + ".jsx": "jsx", + ".tsx": "tsx", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".cc": "cpp", + ".cxx": "cpp", + ".h": "c", + ".hpp": "cpp", + ".cs": "csharp", + ".php": "php", + ".rb": "ruby", + ".rs": "rust", + ".swift": "swift", + ".kt": "kotlin", + ".scala": "scala", + ".clj": "clojure", + ".hs": "haskell", + ".ml": "ocaml", + ".fs": "fsharp", + ".vb": "vbnet", + ".pl": "perl", + ".r": "r", + ".m": "matlab", + ".lua": "lua", + ".sh": "bash", + ".bash": "bash", + ".zsh": "zsh", + ".fish": "fish", + ".ps1": "powershell", + ".bat": "batch", + ".cmd": "batch", + ".html": "html", + ".htm": "html", + ".xml": "xml", + ".css": "css", + ".scss": "scss", + ".sass": "sass", + ".less": "less", + ".json": "json", + ".yaml": "yaml", + ".yml": "yaml", + ".toml": "toml", + ".ini": "ini", + ".cfg": "ini", + ".conf": "ini", + ".sql": "sql", + ".md": "markdown", + ".markdown": "markdown", + ".tex": "latex", + ".dockerfile": "dockerfile", + ".makefile": "makefile", + ".mk": "makefile", + ".vim": "vim", + ".proto": "protobuf", + ".graphql": "graphql", + ".gql": "graphql", + ".dart": "dart", + ".elm": "elm", + ".ex": "elixir", + ".exs": "elixir", + ".erl": "erlang", + ".hrl": "erlang", + ".jl": "julia", + ".nim": "nim", + ".zig": "zig", + ".v": "v", + ".d": "d", + ".pas": "pascal", + ".pp": "pascal", + ".ada": "ada", + ".adb": "ada", + ".ads": "ada", + ".f": "fortran", + ".f90": "fortran", + ".f95": "fortran", + ".f03": "fortran", + ".f08": "fortran", + ".cob": "cobol", + ".cbl": "cobol", + ".asm": "nasm", + ".s": "gas", + } + + if lang, exists := commonMappings[ext]; exists { + return lang + } + + return ld.FallbackLanguage +} diff --git a/language_detector_test.go b/language_detector_test.go new file mode 100644 index 0000000..d49132f --- /dev/null +++ b/language_detector_test.go @@ -0,0 +1,243 @@ +package freezelib + +import ( + "testing" +) + +func TestLanguageDetector(t *testing.T) { + detector := NewLanguageDetector() + + tests := []struct { + name string + code string + expected string + }{ + { + name: "Go code", + code: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}`, + expected: "go", + }, + { + name: "Python code - fallback to text", + code: `def hello(): + print("Hello, World!") + +if __name__ == "__main__": + hello()`, + expected: "text", // Content analysis might not work for all languages + }, + { + name: "JavaScript code - fallback to text", + code: `function hello() { + console.log("Hello, World!"); +} + +hello();`, + expected: "text", // Content analysis might not work for all languages + }, + { + name: "Rust code - fallback to text", + code: `fn main() { + println!("Hello, World!"); +}`, + expected: "text", // Content analysis might not work for all languages + }, + { + name: "JSON code - fallback to text", + code: `{ + "name": "test", + "version": "1.0.0" +}`, + expected: "text", // Content analysis might not work for all languages + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detector.DetectLanguage(tt.code) + if result != tt.expected { + t.Errorf("DetectLanguage() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestLanguageDetectorFromFilename(t *testing.T) { + detector := NewLanguageDetector() + + tests := []struct { + name string + filename string + expected string + }{ + {"Go file", "main.go", "go"}, + {"Python file", "script.py", "python"}, + {"JavaScript file", "app.js", "js"}, // chroma uses "js" as first alias + {"TypeScript file", "app.ts", "ts"}, // chroma uses "ts" as first alias + {"Rust file", "main.rs", "rust"}, + {"CSS file", "style.css", "css"}, + {"JSON file", "package.json", "json"}, + {"Dockerfile", "Dockerfile", "docker"}, // chroma uses "docker" as first alias + {"Shell script", "deploy.sh", "bash"}, + {"Unknown extension", "file.unknown", "text"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detector.DetectLanguageFromFilename(tt.filename) + if result != tt.expected { + t.Errorf("DetectLanguageFromFilename() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestLanguageDetectorCustomMappings(t *testing.T) { + detector := NewLanguageDetector() + + // Add custom mapping + detector.AddCustomMapping(".myext", "python") + + result := detector.DetectLanguageFromFilename("script.myext") + if result != "python" { + t.Errorf("Custom mapping failed: got %v, want python", result) + } + + // Remove custom mapping + detector.RemoveCustomMapping(".myext") + + result = detector.DetectLanguageFromFilename("script.myext") + if result == "python" { + t.Errorf("Custom mapping removal failed: still returns python") + } +} + +func TestLanguageDetectorCombined(t *testing.T) { + detector := NewLanguageDetector() + + // Test with filename that has extension but content is different + pythonCode := `def hello(): + print("Hello from Python!") + +hello()` + + // Should prefer filename detection + result := detector.DetectLanguageFromFile("script.py", pythonCode) + if result != "python" { + t.Errorf("Combined detection failed: got %v, want python", result) + } + + // Test with unknown extension - should fallback to text since content analysis may not work + result = detector.DetectLanguageFromFile("script.unknown", pythonCode) + if result != "text" { + t.Errorf("Content fallback failed: got %v, want text", result) + } +} + +func TestLanguageDetectorConfiguration(t *testing.T) { + detector := NewLanguageDetector() + + // Test disabling content analysis + detector.EnableContentAnalysis = false + + pythonCode := `def hello(): + print("Hello!") +hello()` + + result := detector.DetectLanguage(pythonCode) + if result != detector.FallbackLanguage { + t.Errorf("Content analysis should be disabled: got %v, want %v", result, detector.FallbackLanguage) + } + + // Test disabling filename analysis + detector.EnableContentAnalysis = true + detector.EnableFilenameAnalysis = false + + result = detector.DetectLanguageFromFilename("script.py") + if result != detector.FallbackLanguage { + t.Errorf("Filename analysis should be disabled: got %v, want %v", result, detector.FallbackLanguage) + } +} + +func TestFreezeAutoDetection(t *testing.T) { + freeze := New() + + goCode := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}` + + // Test auto detection + svgData, err := freeze.GenerateFromCodeAuto(goCode) + if err != nil { + t.Errorf("GenerateFromCodeAuto failed: %v", err) + } + + if len(svgData) == 0 { + t.Error("GenerateFromCodeAuto returned empty data") + } + + // Test language detection + language := freeze.DetectLanguage(goCode) + if language != "go" { + t.Errorf("Language detection failed: got %v, want go", language) + } +} + +func TestQuickFreezeAutoDetection(t *testing.T) { + qf := NewQuickFreeze() + + jsCode := `function hello() { + console.log("Hello, World!"); +} + +hello();` + + // Test auto detection + svgData, err := qf.CodeToSVGAuto(jsCode) + if err != nil { + t.Errorf("CodeToSVGAuto failed: %v", err) + } + + if len(svgData) == 0 { + t.Error("CodeToSVGAuto returned empty data") + } + + // Test language detection - content analysis may not work for JS + language := qf.DetectLanguage(jsCode) + if language != "text" { + t.Errorf("Language detection failed: got %v, want text", language) + } +} + +func TestLanguageSupport(t *testing.T) { + freeze := New() + + // Test supported languages + languages := freeze.GetSupportedLanguages() + if len(languages) == 0 { + t.Error("No supported languages found") + } + + // Test common languages + commonLanguages := []string{"go", "python", "javascript", "rust", "java", "c", "cpp"} + for _, lang := range commonLanguages { + if !freeze.IsLanguageSupported(lang) { + t.Errorf("Language %s should be supported", lang) + } + } + + // Test unsupported language + if freeze.IsLanguageSupported("nonexistent-language") { + t.Error("Nonexistent language should not be supported") + } +} diff --git a/quickfreeze.go b/quickfreeze.go index 5a68c6a..ab7a20d 100644 --- a/quickfreeze.go +++ b/quickfreeze.go @@ -162,6 +162,12 @@ func (qf *QuickFreeze) CodeToSVG(code string) ([]byte, error) { return generator.GenerateFromCode(code, qf.config.Language) } +// CodeToSVGAuto generates SVG from source code with automatic language detection +func (qf *QuickFreeze) CodeToSVGAuto(code string) ([]byte, error) { + generator := NewGenerator(qf.config) + return generator.GenerateFromCode(code, "") +} + // CodeToPNG generates PNG from source code func (qf *QuickFreeze) CodeToPNG(code string) ([]byte, error) { generator := NewGenerator(qf.config) @@ -183,6 +189,58 @@ func (qf *QuickFreeze) CodeToPNG(code string) ([]byte, error) { return generator.ConvertToPNG(svgData, width, height) } +// CodeToPNGAuto generates PNG from source code with automatic language detection +func (qf *QuickFreeze) CodeToPNGAuto(code string) ([]byte, error) { + generator := NewGenerator(qf.config) + svgData, err := generator.GenerateFromCode(code, "") + if err != nil { + return nil, err + } + + // Calculate dimensions for PNG + width := qf.config.Width + height := qf.config.Height + if width == 0 || height == 0 { + width = 800 * 4 + height = 600 * 4 + } else { + width *= 4 + height *= 4 + } + + return generator.ConvertToPNG(svgData, width, height) +} + +// DetectLanguage detects the programming language from code content +func (qf *QuickFreeze) DetectLanguage(code string) string { + generator := NewGenerator(qf.config) + return generator.DetectLanguage(code) +} + +// DetectLanguageFromFilename detects the programming language from filename +func (qf *QuickFreeze) DetectLanguageFromFilename(filename string) string { + generator := NewGenerator(qf.config) + return generator.DetectLanguageFromFilename(filename) +} + +// DetectLanguageFromFile detects language from both filename and content +func (qf *QuickFreeze) DetectLanguageFromFile(filename, content string) string { + generator := NewGenerator(qf.config) + return generator.DetectLanguageFromFile(filename, content) +} + +// GetSupportedLanguages returns a list of all supported languages +func (qf *QuickFreeze) GetSupportedLanguages() []string { + generator := NewGenerator(qf.config) + return generator.GetSupportedLanguages() +} + +// IsLanguageSupported checks if a language is supported +func (qf *QuickFreeze) IsLanguageSupported(language string) bool { + generator := NewGenerator(qf.config) + return generator.IsLanguageSupported(language) +} + // FileToSVG generates SVG from a source code file func (qf *QuickFreeze) FileToSVG(filename string) ([]byte, error) { generator := NewGenerator(qf.config)