added automatic language detection and API

This commit is contained in:
2025-07-18 23:06:37 +08:00
parent d2630e4503
commit 5783f40de7
8 changed files with 1400 additions and 17 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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 {

273
language_detector.go Normal file
View File

@@ -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
}

243
language_detector_test.go Normal file
View File

@@ -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")
}
}

View File

@@ -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)