commit d2630e4503d7b30b692d580b4fe59fdaefead398 Author: landaiqing Date: Fri Jul 18 19:02:23 2025 +0800 :sparkles: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..815ad95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,264 @@ +### GoLand+all template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +### GoLand template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand+iml template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/freezelib.iml b/.idea/freezelib.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/freezelib.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6ca929f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cee7643 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 landaiqing + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a35c730 --- /dev/null +++ b/README.md @@ -0,0 +1,287 @@ +# FreezeLib - Go Library for Beautiful Code Screenshots + +**Language / 语言**: [English](README.md) | [中文](README_CN.md) + +**Documentation / 文档**: [Usage Guide (English)](USAGE_EN.md) | [使用指南 (中文)](USAGE.md) + +FreezeLib is a Go library for generating beautiful screenshots of code and terminal output. It's based on the popular [freeze](https://github.com/charmbracelet/freeze) CLI tool by Charm, but redesigned as a reusable library for Go applications. + +## Features + +- 🎨 **Syntax Highlighting**: Support for 100+ programming languages +- 🖼️ **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 +- 📏 **Line Numbers**: Optional line numbering +- 🌈 **ANSI Support**: Render colored terminal output +- ⚡ **Easy API**: Simple and chainable API design +- 🎯 **Presets**: Pre-configured styles for common use cases +- 🔧 **Highly Customizable**: Fine-tune every aspect of the output + +## Installation + +```bash +go get github.com/landaiqing/freezelib +``` + +## Quick Start + +### Basic Usage + +```go +package main + +import ( + "os" + "github.com/landaiqing/freezelib" +) + +func main() { + // Create a new freeze instance + freeze := freezelib.New() + + // Go code to screenshot + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + panic(err) + } + + // Save to file + os.WriteFile("hello.svg", svgData, 0644) +} +``` + +### QuickFreeze API + +For a more fluent experience, use the QuickFreeze API: + +```go +qf := freezelib.NewQuickFreeze() + +svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + WithShadow(). + WithLineNumbers(). + CodeToSVG(code) +``` + +## API Reference + +### Core Types + +#### Freeze + +The main interface for generating screenshots: + +```go +freeze := freezelib.New() // Default config +freeze := freezelib.NewWithConfig(config) // Custom config +freeze := freezelib.NewWithPreset("dark") // Preset config +``` + +#### QuickFreeze + +Simplified, chainable API: + +```go +qf := freezelib.NewQuickFreeze() +qf := freezelib.NewQuickFreezeWithPreset("terminal") +``` + +### Generation Methods + +#### From Code String +```go +svgData, err := freeze.GenerateFromCode(code, "python") +pngData, err := freeze.GeneratePNGFromCode(code, "python") +``` + +#### From File +```go +svgData, err := freeze.GenerateFromFile("main.go") +pngData, err := freeze.GeneratePNGFromFile("main.go") +``` + +#### From ANSI Terminal Output +```go +terminalOutput := "\033[32mSUCCESS\033[0m: Build completed" +svgData, err := freeze.GenerateFromANSI(terminalOutput) +pngData, err := freeze.GeneratePNGFromANSI(terminalOutput) +``` + +#### From Reader +```go +svgData, err := freeze.GenerateFromReader(reader, "javascript") +``` + +### Configuration + +#### Basic Configuration +```go +config := freezelib.DefaultConfig() +config.SetTheme("github-dark") +config.SetFont("JetBrains Mono", 14) +config.SetBackground("#1e1e1e") +config.SetWindow(true) +config.SetLineNumbers(true) +``` + +#### Advanced Configuration +```go +config.SetPadding(20) // All sides +config.SetPadding(20, 40) // Vertical, horizontal +config.SetPadding(20, 40, 20, 40) // Top, right, bottom, left + +config.SetShadow(20, 0, 10) // Blur, X offset, Y offset +config.SetBorder(1, 8, "#333") // Width, radius, color +config.SetDimensions(800, 600) // Width, height +config.SetLines(10, 20) // Line range (1-indexed) +``` + +### Presets + +FreezeLib comes with several built-in presets: + +```go +// Available presets +presets := []string{ + "base", // Simple, clean + "full", // macOS-style with window controls + "terminal", // Optimized for terminal output + "presentation", // High contrast for presentations + "minimal", // Minimal styling + "dark", // Dark theme + "light", // Light theme + "retro", // Retro terminal style + "neon", // Neon/cyberpunk style + "compact", // Compact for small snippets +} + +freeze := freezelib.NewWithPreset("dark") +``` + +### Chainable Methods + +Both `Freeze` and `QuickFreeze` support method chaining: + +```go +freeze := freezelib.New(). + WithTheme("monokai"). + WithFont("Cascadia Code", 15). + WithWindow(true). + WithShadow(20, 0, 10). + WithLineNumbers(true) + +svgData, err := freeze.GenerateFromCode(code, "rust") +``` + +## Examples + +### Terminal Output Screenshot + +```go +freeze := freezelib.NewWithPreset("terminal") + +ansiOutput := "\033[32m✓ Tests passed\033[0m\n" + + "\033[31m✗ Build failed\033[0m\n" + + "\033[33m⚠ Warning: deprecated API\033[0m" + +svgData, err := freeze.GenerateFromANSI(ansiOutput) +``` + +### Custom Styling + +```go +config := freezelib.DefaultConfig() +config.Theme = "github" +config.Background = "#f6f8fa" +config.Font.Family = "SF Mono" +config.Font.Size = 16 +config.SetPadding(30) +config.SetMargin(20) +config.Window = true +config.ShowLineNumbers = true +config.Border.Radius = 12 +config.Shadow.Blur = 25 + +freeze := freezelib.NewWithConfig(config) +``` + +### Batch Processing + +```go +files := []string{"main.go", "config.go", "utils.go"} + +for _, file := range files { + svgData, err := freeze.GenerateFromFile(file) + if err != nil { + continue + } + + outputFile := strings.TrimSuffix(file, ".go") + ".svg" + os.WriteFile(outputFile, svgData, 0644) +} +``` + +## Supported Languages + +FreezeLib supports syntax highlighting for 100+ programming languages including: + +- Go, Rust, Python, JavaScript, TypeScript +- C, C++, C#, Java, Kotlin, Swift +- HTML, CSS, SCSS, JSON, YAML, XML +- Shell, PowerShell, Dockerfile +- SQL, GraphQL, Markdown +- And many more... + +## Supported Themes + +Popular themes include: +- `github` / `github-dark` +- `dracula` +- `monokai` +- `solarized-dark` / `solarized-light` +- `nord` +- `one-dark` +- `material` +- `vim` +- And many more... + +## Error Handling + +```go +svgData, err := freeze.GenerateFromCode(code, "go") +if err != nil { + // Handle specific errors + switch { + case strings.Contains(err.Error(), "language"): + // Language detection failed + case strings.Contains(err.Error(), "config"): + // Configuration error + default: + // Other errors + } +} +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +MIT License - see the [LICENSE](./LICENSE) file for details. + +## Acknowledgments + +This library is based on the excellent [freeze](https://github.com/charmbracelet/freeze) CLI tool by [Charm](https://charm.sh). Special thanks to the Charm team for creating such a beautiful tool. diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..6814c0f --- /dev/null +++ b/README_CN.md @@ -0,0 +1,287 @@ +# FreezeLib - 美观代码截图的 Go 库 + +**Language / 语言**: [English](README.md) | [中文](README_CN.md) + +**Documentation / 文档**: [Usage Guide (English)](USAGE_EN.md) | [使用指南 (中文)](USAGE.md) + +FreezeLib 是一个用于生成美观代码和终端输出截图的 Go 库。它基于 Charm 团队广受欢迎的 [freeze](https://github.com/charmbracelet/freeze) CLI 工具,但重新设计为可在 Go 应用程序中重复使用的库。 + +## 特性 + +- 🎨 **语法高亮**: 支持 100+ 种编程语言 +- 🖼️ **多种输出格式**: 生成 SVG 和 PNG 图像 +- 🎭 **丰富主题**: 内置主题包括 GitHub、Dracula、Monokai 等 +- 🪟 **窗口控件**: macOS 风格的窗口装饰 +- 📏 **行号**: 可选的行号显示 +- 🌈 **ANSI 支持**: 渲染彩色终端输出 +- ⚡ **简易 API**: 简单且可链式调用的 API 设计 +- 🎯 **预设配置**: 常见用例的预配置样式 +- 🔧 **高度可定制**: 精细调整输出的每个方面 + +## 安装 + +```bash +go get github.com/landaiqing/freezelib +``` + +## 快速开始 + +### 基本用法 + +```go +package main + +import ( + "os" + "github.com/landaiqing/freezelib" +) + +func main() { + // 创建新的 freeze 实例 + freeze := freezelib.New() + + // 要截图的 Go 代码 + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}` + + // 生成 SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + panic(err) + } + + // 保存到文件 + os.WriteFile("hello.svg", svgData, 0644) +} +``` + +### QuickFreeze API + +为了更流畅的体验,使用 QuickFreeze API: + +```go +qf := freezelib.NewQuickFreeze() + +svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + WithShadow(). + WithLineNumbers(). + CodeToSVG(code) +``` + +## API 参考 + +### 核心类型 + +#### Freeze + +生成截图的主要接口: + +```go +freeze := freezelib.New() // 默认配置 +freeze := freezelib.NewWithConfig(config) // 自定义配置 +freeze := freezelib.NewWithPreset("dark") // 预设配置 +``` + +#### QuickFreeze + +简化的链式 API: + +```go +qf := freezelib.NewQuickFreeze() +qf := freezelib.NewQuickFreezeWithPreset("terminal") +``` + +### 生成方法 + +#### 从代码字符串 +```go +svgData, err := freeze.GenerateFromCode(code, "python") +pngData, err := freeze.GeneratePNGFromCode(code, "python") +``` + +#### 从文件 +```go +svgData, err := freeze.GenerateFromFile("main.go") +pngData, err := freeze.GeneratePNGFromFile("main.go") +``` + +#### 从 ANSI 终端输出 +```go +terminalOutput := "\033[32mSUCCESS\033[0m: Build completed" +svgData, err := freeze.GenerateFromANSI(terminalOutput) +pngData, err := freeze.GeneratePNGFromANSI(terminalOutput) +``` + +#### 从 Reader +```go +svgData, err := freeze.GenerateFromReader(reader, "javascript") +``` + +### 配置 + +#### 基本配置 +```go +config := freezelib.DefaultConfig() +config.SetTheme("github-dark") +config.SetFont("JetBrains Mono", 14) +config.SetBackground("#1e1e1e") +config.SetWindow(true) +config.SetLineNumbers(true) +``` + +#### 高级配置 +```go +config.SetPadding(20) // 所有边 +config.SetPadding(20, 40) // 垂直,水平 +config.SetPadding(20, 40, 20, 40) // 上,右,下,左 + +config.SetShadow(20, 0, 10) // 模糊,X 偏移,Y 偏移 +config.SetBorder(1, 8, "#333") // 宽度,圆角,颜色 +config.SetDimensions(800, 600) // 宽度,高度 +config.SetLines(10, 20) // 行范围(1-indexed) +``` + +### 预设 + +FreezeLib 提供了几个内置预设: + +```go +// 可用预设 +presets := []string{ + "base", // 简洁干净 + "full", // macOS 风格窗口控件 + "terminal", // 终端输出优化 + "presentation", // 演示高对比度 + "minimal", // 极简样式 + "dark", // 深色主题 + "light", // 浅色主题 + "retro", // 复古终端风格 + "neon", // 霓虹/赛博朋克风格 + "compact", // 小代码片段紧凑型 +} + +freeze := freezelib.NewWithPreset("dark") +``` + +### 链式方法 + +`Freeze` 和 `QuickFreeze` 都支持方法链: + +```go +freeze := freezelib.New(). + WithTheme("monokai"). + WithFont("Cascadia Code", 15). + WithWindow(true). + WithShadow(20, 0, 10). + WithLineNumbers(true) + +svgData, err := freeze.GenerateFromCode(code, "rust") +``` + +## 示例 + +### 终端输出截图 + +```go +freeze := freezelib.NewWithPreset("terminal") + +ansiOutput := "\033[32m✓ Tests passed\033[0m\n" + + "\033[31m✗ Build failed\033[0m\n" + + "\033[33m⚠ Warning: deprecated API\033[0m" + +svgData, err := freeze.GenerateFromANSI(ansiOutput) +``` + +### 自定义样式 + +```go +config := freezelib.DefaultConfig() +config.Theme = "github" +config.Background = "#f6f8fa" +config.Font.Family = "SF Mono" +config.Font.Size = 16 +config.SetPadding(30) +config.SetMargin(20) +config.Window = true +config.ShowLineNumbers = true +config.Border.Radius = 12 +config.Shadow.Blur = 25 + +freeze := freezelib.NewWithConfig(config) +``` + +### 批量处理 + +```go +files := []string{"main.go", "config.go", "utils.go"} + +for _, file := range files { + svgData, err := freeze.GenerateFromFile(file) + if err != nil { + continue + } + + outputFile := strings.TrimSuffix(file, ".go") + ".svg" + os.WriteFile(outputFile, svgData, 0644) +} +``` + +## 支持的语言 + +FreezeLib 支持 100+ 种编程语言的语法高亮,包括: + +- Go, Rust, Python, JavaScript, TypeScript +- C, C++, C#, Java, Kotlin, Swift +- HTML, CSS, SCSS, JSON, YAML, XML +- Shell, PowerShell, Dockerfile +- SQL, GraphQL, Markdown +- 等等... + +## 支持的主题 + +流行主题包括: +- `github` / `github-dark` +- `dracula` +- `monokai` +- `solarized-dark` / `solarized-light` +- `nord` +- `one-dark` +- `material` +- `vim` +- 等等... + +## 错误处理 + +```go +svgData, err := freeze.GenerateFromCode(code, "go") +if err != nil { + // 处理特定错误 + switch { + case strings.Contains(err.Error(), "language"): + // 语言检测失败 + case strings.Contains(err.Error(), "config"): + // 配置错误 + default: + // 其他错误 + } +} +``` + +## 贡献 + +欢迎贡献!请随时提交 Pull Request。 + +## 许可证 + +MIT 许可证 - 详见 [LICENSE](./LICENSE) 文件。 + +## 致谢 + +本库基于 [Charm](https://charm.sh) 团队出色的 [freeze](https://github.com/charmbracelet/freeze) CLI 工具。特别感谢 Charm 团队创造了如此美观的工具。 diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..3ddf52a --- /dev/null +++ b/USAGE.md @@ -0,0 +1,264 @@ +# FreezeLib 使用指南 + +**Language / 语言**: [English](USAGE_EN.md) | [中文](USAGE.md) + +**Main Documentation / 主要文档**: [README (English)](README.md) | [README (中文)](README_CN.md) + +FreezeLib 是一个基于 Charm 的 freeze CLI 工具重构的 Go 公共库,用于生成美观的代码截图。 + +## 🚀 快速开始 + +### 基本用法 + +```go +package main + +import ( + "github.com/landaiqing/freezelib" + "os" +) + +func main() { + // 创建 freeze 实例 + freeze := freezelib.New() + + // 要截图的代码 + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}` + + // 生成 SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + panic(err) + } + + // 保存到文件 + os.WriteFile("hello.svg", svgData, 0644) +} +``` + +### 链式调用 API + +```go +// 使用 QuickFreeze 进行链式调用 +qf := freezelib.NewQuickFreeze() + +svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + WithShadow(). + WithLineNumbers(). + WithLanguage("javascript"). + CodeToSVG(code) +``` + +## 📋 主要功能 + +### 1. 多种输入方式 + +```go +// 从代码字符串生成 +svgData, err := freeze.GenerateFromCode(code, "python") + +// 从文件生成 +svgData, err := freeze.GenerateFromFile("main.go") + +// 从 ANSI 终端输出生成 +ansiOutput := "\033[32m✓ SUCCESS\033[0m: Build completed" +svgData, err := freeze.GenerateFromANSI(ansiOutput) + +// 从 Reader 生成 +svgData, err := freeze.GenerateFromReader(reader, "javascript") +``` + +### 2. 多种输出格式 + +```go +// 生成 SVG +svgData, err := freeze.GenerateFromCode(code, "go") + +// 生成 PNG +pngData, err := freeze.GeneratePNGFromCode(code, "go") + +// 直接保存到文件 +err := freeze.SaveCodeToFile(code, "go", "output.svg") +err := freeze.SaveCodeToFile(code, "go", "output.png") // 自动检测格式 +``` + +### 3. 预设配置 + +```go +// 使用预设配置 +freeze := freezelib.NewWithPreset("dark") // 深色主题 +freeze := freezelib.NewWithPreset("terminal") // 终端风格 +freeze := freezelib.NewWithPreset("presentation") // 演示风格 + +// 可用预设 +presets := []string{ + "base", // 基础样式 + "full", // macOS 风格 + "terminal", // 终端优化 + "presentation", // 演示优化 + "minimal", // 极简风格 + "dark", // 深色主题 + "light", // 浅色主题 + "retro", // 复古风格 + "neon", // 霓虹风格 + "compact", // 紧凑风格 +} +``` + +### 4. 自定义配置 + +```go +config := freezelib.DefaultConfig() + +// 基本设置 +config.SetTheme("github-dark") +config.SetFont("JetBrains Mono", 14) +config.SetBackground("#1e1e1e") +config.SetLanguage("python") + +// 布局设置 +config.SetPadding(20) // 所有边 +config.SetPadding(20, 40) // 垂直,水平 +config.SetPadding(20, 40, 20, 40) // 上,右,下,左 +config.SetMargin(15) +config.SetDimensions(800, 600) + +// 装饰效果 +config.SetWindow(true) // 窗口控件 +config.SetLineNumbers(true) // 行号 +config.SetShadow(20, 0, 10) // 阴影:模糊,X偏移,Y偏移 +config.SetBorder(1, 8, "#333") // 边框:宽度,圆角,颜色 + +// 行范围(1-indexed) +config.SetLines(10, 20) // 只截取第10-20行 + +freeze := freezelib.NewWithConfig(config) +``` + +## 🎨 支持的主题 + +- `github` / `github-dark` +- `dracula` +- `monokai` +- `solarized-dark` / `solarized-light` +- `nord` +- `one-dark` +- `material` +- `vim` +- 等等... + +## 💻 支持的语言 + +支持 100+ 种编程语言,包括: +- Go, Rust, Python, JavaScript, TypeScript +- C, C++, C#, Java, Kotlin, Swift +- HTML, CSS, SCSS, JSON, YAML, XML +- Shell, PowerShell, Dockerfile +- SQL, GraphQL, Markdown +- 等等... + +## 🔧 高级用法 + +### 批量处理 + +```go +files := []string{"main.go", "config.go", "utils.go"} + +for _, file := range files { + svgData, err := freeze.GenerateFromFile(file) + if err != nil { + continue + } + + outputFile := strings.TrimSuffix(file, ".go") + ".svg" + os.WriteFile(outputFile, svgData, 0644) +} +``` + +### 终端输出截图 + +```go +freeze := freezelib.NewWithPreset("terminal") + +ansiOutput := "\033[32m✓ Tests passed\033[0m\n" + + "\033[31m✗ Build failed\033[0m\n" + + "\033[33m⚠ Warning: deprecated API\033[0m" + +svgData, err := freeze.GenerateFromANSI(ansiOutput) +``` + +### 链式方法 + +```go +freeze := freezelib.New(). + WithTheme("monokai"). + WithFont("Cascadia Code", 15). + WithWindow(true). + WithShadow(20, 0, 10). + WithLineNumbers(true) + +svgData, err := freeze.GenerateFromCode(code, "rust") +``` + +## 📊 性能优化建议 + +1. **重用实例**:创建一个 `Freeze` 实例并重复使用 +2. **选择合适格式**:网页用 SVG,演示用 PNG +3. **设置具体尺寸**:指定尺寸可提高性能 +4. **批量操作**:在单个会话中处理多个文件 + +## 🐛 错误处理 + +```go +svgData, err := freeze.GenerateFromCode(code, "go") +if err != nil { + switch { + case strings.Contains(err.Error(), "language"): + // 语言检测失败 + case strings.Contains(err.Error(), "config"): + // 配置错误 + default: + // 其他错误 + } +} +``` + +## 📁 项目结构 + +``` +freezelib/ +├── freeze.go # 主要 API 接口 +├── config.go # 配置结构体 +├── generator.go # 核心生成逻辑 +├── quickfreeze.go # 简化 API +├── presets.go # 预设配置 +├── ansi.go # ANSI 处理 +├── svg/ # SVG 处理 +├── font/ # 字体处理 +├── example/ # 使用示例 +└── README.md # 详细文档 +``` + +## 🤝 与原版 freeze 的区别 + +| 特性 | 原版 freeze | FreezeLib | +|------|-------------|-----------| +| 使用方式 | CLI 工具 | Go 库 | +| 集成方式 | 命令行调用 | 直接导入 | +| 配置方式 | 命令行参数/配置文件 | Go 结构体 | +| 扩展性 | 有限 | 高度可扩展 | +| 性能 | 进程启动开销 | 内存中处理 | + +## 📝 示例代码 + +查看 `examples` 目录中的完整示例: + +这将生成多个示例 SVG 文件,展示库的各种功能。 diff --git a/USAGE_EN.md b/USAGE_EN.md new file mode 100644 index 0000000..b107aab --- /dev/null +++ b/USAGE_EN.md @@ -0,0 +1,264 @@ +# FreezeLib Usage Guide + +**Language / 语言**: [English](USAGE_EN.md) | [中文](USAGE.md) + +**Main Documentation / 主要文档**: [README (English)](README.md) | [README (中文)](README_CN.md) + +FreezeLib is a Go library refactored from Charm's freeze CLI tool for generating beautiful code screenshots. + +## 🚀 Quick Start + +### Basic Usage + +```go +package main + +import ( + "github.com/landaiqing/freezelib" + "os" +) + +func main() { + // Create freeze instance + freeze := freezelib.New() + + // Code to screenshot + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + panic(err) + } + + // Save to file + os.WriteFile("hello.svg", svgData, 0644) +} +``` + +### Chainable API + +```go +// Use QuickFreeze for method chaining +qf := freezelib.NewQuickFreeze() + +svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + WithShadow(). + WithLineNumbers(). + WithLanguage("javascript"). + CodeToSVG(code) +``` + +## 📋 Main Features + +### 1. Multiple Input Methods + +```go +// Generate from code string +svgData, err := freeze.GenerateFromCode(code, "python") + +// Generate from file +svgData, err := freeze.GenerateFromFile("main.go") + +// Generate from ANSI terminal output +ansiOutput := "\033[32m✓ SUCCESS\033[0m: Build completed" +svgData, err := freeze.GenerateFromANSI(ansiOutput) + +// Generate from Reader +svgData, err := freeze.GenerateFromReader(reader, "javascript") +``` + +### 2. Multiple Output Formats + +```go +// Generate SVG +svgData, err := freeze.GenerateFromCode(code, "go") + +// Generate PNG +pngData, err := freeze.GeneratePNGFromCode(code, "go") + +// Save directly to file +err := freeze.SaveCodeToFile(code, "go", "output.svg") +err := freeze.SaveCodeToFile(code, "go", "output.png") // Auto-detect format +``` + +### 3. Preset Configurations + +```go +// Use preset configurations +freeze := freezelib.NewWithPreset("dark") // Dark theme +freeze := freezelib.NewWithPreset("terminal") // Terminal style +freeze := freezelib.NewWithPreset("presentation") // Presentation style + +// Available presets +presets := []string{ + "base", // Basic style + "full", // macOS style + "terminal", // Terminal optimized + "presentation", // Presentation optimized + "minimal", // Minimal style + "dark", // Dark theme + "light", // Light theme + "retro", // Retro style + "neon", // Neon style + "compact", // Compact style +} +``` + +### 4. Custom Configuration + +```go +config := freezelib.DefaultConfig() + +// Basic settings +config.SetTheme("github-dark") +config.SetFont("JetBrains Mono", 14) +config.SetBackground("#1e1e1e") +config.SetLanguage("python") + +// Layout settings +config.SetPadding(20) // All sides +config.SetPadding(20, 40) // Vertical, horizontal +config.SetPadding(20, 40, 20, 40) // Top, right, bottom, left +config.SetMargin(15) +config.SetDimensions(800, 600) + +// Decorative effects +config.SetWindow(true) // Window controls +config.SetLineNumbers(true) // Line numbers +config.SetShadow(20, 0, 10) // Shadow: blur, X offset, Y offset +config.SetBorder(1, 8, "#333") // Border: width, radius, color + +// Line range (1-indexed) +config.SetLines(10, 20) // Only capture lines 10-20 + +freeze := freezelib.NewWithConfig(config) +``` + +## 🎨 Supported Themes + +- `github` / `github-dark` +- `dracula` +- `monokai` +- `solarized-dark` / `solarized-light` +- `nord` +- `one-dark` +- `material` +- `vim` +- And more... + +## 💻 Supported Languages + +Supports 100+ programming languages including: +- Go, Rust, Python, JavaScript, TypeScript +- C, C++, C#, Java, Kotlin, Swift +- HTML, CSS, SCSS, JSON, YAML, XML +- Shell, PowerShell, Dockerfile +- SQL, GraphQL, Markdown +- And more... + +## 🔧 Advanced Usage + +### Batch Processing + +```go +files := []string{"main.go", "config.go", "utils.go"} + +for _, file := range files { + svgData, err := freeze.GenerateFromFile(file) + if err != nil { + continue + } + + outputFile := strings.TrimSuffix(file, ".go") + ".svg" + os.WriteFile(outputFile, svgData, 0644) +} +``` + +### Terminal Output Screenshots + +```go +freeze := freezelib.NewWithPreset("terminal") + +ansiOutput := "\033[32m✓ Tests passed\033[0m\n" + + "\033[31m✗ Build failed\033[0m\n" + + "\033[33m⚠ Warning: deprecated API\033[0m" + +svgData, err := freeze.GenerateFromANSI(ansiOutput) +``` + +### Method Chaining + +```go +freeze := freezelib.New(). + WithTheme("monokai"). + WithFont("Cascadia Code", 15). + WithWindow(true). + WithShadow(20, 0, 10). + WithLineNumbers(true) + +svgData, err := freeze.GenerateFromCode(code, "rust") +``` + +## 📊 Performance Optimization Tips + +1. **Reuse instances**: Create one `Freeze` instance and reuse it +2. **Choose appropriate formats**: SVG for web, PNG for presentations +3. **Set specific dimensions**: Specifying dimensions improves performance +4. **Batch operations**: Process multiple files in a single session + +## 🐛 Error Handling + +```go +svgData, err := freeze.GenerateFromCode(code, "go") +if err != nil { + switch { + case strings.Contains(err.Error(), "language"): + // Language detection failed + case strings.Contains(err.Error(), "config"): + // Configuration error + default: + // Other errors + } +} +``` + +## 📁 Project Structure + +``` +freezelib/ +├── freeze.go # Main API interface +├── config.go # Configuration structs +├── generator.go # Core generation logic +├── quickfreeze.go # Simplified API +├── presets.go # Preset configurations +├── ansi.go # ANSI processing +├── svg/ # SVG processing +├── font/ # Font processing +├── example/ # Usage examples +└── README.md # Detailed documentation +``` + +## 🤝 Differences from Original freeze + +| Feature | Original freeze | FreezeLib | +|---------|----------------|-----------| +| Usage | CLI tool | Go library | +| Integration | Command line calls | Direct import | +| Configuration | CLI args/config files | Go structs | +| Extensibility | Limited | Highly extensible | +| Performance | Process startup overhead | In-memory processing | + +## 📝 Example Code + +Check the complete examples in the `examples` directory: + +This will generate multiple example SVG files showcasing various features of the library. diff --git a/ansi.go b/ansi.go new file mode 100644 index 0000000..9f24f26 --- /dev/null +++ b/ansi.go @@ -0,0 +1,299 @@ +package freezelib + +import ( + "fmt" + "strings" + + "github.com/beevik/etree" + "github.com/charmbracelet/x/ansi" + "github.com/mattn/go-runewidth" +) + +// dispatcher handles ANSI escape sequences and converts them to SVG +type dispatcher struct { + lines []*etree.Element + svg *etree.Element + config *Config + scale float64 + row int + col int + bg *etree.Element + bgWidth int +} + +// newDispatcher creates a new ANSI dispatcher +func newDispatcher(lines []*etree.Element, svg *etree.Element, config *Config, scale float64) *dispatcher { + return &dispatcher{ + lines: lines, + svg: svg, + config: config, + scale: scale, + row: 0, + col: 0, + } +} + +// Print handles printable characters +func (p *dispatcher) Print(r rune) { + p.row = clamp(p.row, 0, len(p.lines)-1) + // insert the rune in the last tspan + children := p.lines[p.row].ChildElements() + var lastChild *etree.Element + isFirstChild := len(children) == 0 + if isFirstChild { + lastChild = etree.NewElement("tspan") + lastChild.CreateAttr("xml:space", "preserve") + p.lines[p.row].AddChild(lastChild) + } else { + lastChild = children[len(children)-1] + } + + if runewidth.RuneWidth(r) > 1 { + newChild := lastChild.Copy() + newChild.SetText(string(r)) + newChild.CreateAttr("dx", fmt.Sprintf("%.2fpx", (p.config.Font.Size/5)*p.scale)) + p.lines[p.row].AddChild(newChild) + } else { + lastChild.SetText(lastChild.Text() + string(r)) + } + + p.col += runewidth.RuneWidth(r) + if p.bg != nil { + p.bgWidth += runewidth.RuneWidth(r) + } +} + +// Execute handles control characters +func (p *dispatcher) Execute(code byte) { + if code == '\t' { + for p.col%16 != 0 { + p.Print(' ') + } + } + if code == '\n' { + p.endBackground() + p.row++ + p.col = 0 + } +} + +// endBackground ends the current background span +func (p *dispatcher) endBackground() { + if p.bg == nil { + return + } + p.bg.CreateAttr("width", fmt.Sprintf("%.2fpx", float64(p.bgWidth)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) + p.bg = nil + p.bgWidth = 0 +} + +// CsiDispatch handles CSI (Control Sequence Introducer) sequences +func (p *dispatcher) CsiDispatch(cmd ansi.Cmd, params ansi.Params) { + if cmd != 'm' { + // ignore incomplete or non Style (SGR) sequences + return + } + + span := etree.NewElement("tspan") + span.CreateAttr("xml:space", "preserve") + reset := func() { + // reset ANSI, this is done by creating a new empty tspan, + // which would reset all the styles such that when text is appended to the last + // child of this line there is no styling applied. + if p.row < len(p.lines) { + p.lines[p.row].AddChild(span) + } + p.endBackground() + } + + if len(params) == 0 { + // zero params means reset + reset() + return + } + + var i int + for i < len(params) { + v := params[i].Param(0) + switch v { + case 0: + reset() + case 1: + // Bold - not implemented in SVG for now + p.lines[p.row].AddChild(span) + case 9: + span.CreateAttr("text-decoration", "line-through") + p.lines[p.row].AddChild(span) + case 3: + span.CreateAttr("font-style", "italic") + p.lines[p.row].AddChild(span) + case 4: + span.CreateAttr("text-decoration", "underline") + p.lines[p.row].AddChild(span) + case 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97: + span.CreateAttr("fill", ansiPalette[v]) + p.lines[p.row].AddChild(span) + case 38: + i++ + if i < len(params) { + switch params[i].Param(0) { + case 5: + if i+1 < len(params) { + n := params[i+1].Param(0) + i++ + fill := palette[n] + span.CreateAttr("fill", fill) + p.lines[p.row].AddChild(span) + } + case 2: + if i+3 < len(params) { + r := params[i+1].Param(0) + g := params[i+2].Param(0) + b := params[i+3].Param(0) + i += 3 + fill := fmt.Sprintf("rgb(%d,%d,%d)", r, g, b) + span.CreateAttr("fill", fill) + p.lines[p.row].AddChild(span) + } + } + } + case 40, 41, 42, 43, 44, 45, 46, 47, 100, 101, 102, 103, 104, 105, 106, 107: + // Background colors + p.endBackground() + p.bg = etree.NewElement("rect") + p.bg.CreateAttr("fill", ansiPalette[v-10]) + p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) + p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) + p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) + p.svg.InsertChildAt(0, p.bg) + case 48: + i++ + if i < len(params) { + switch params[i].Param(0) { + case 5: + if i+1 < len(params) { + n := params[i+1].Param(0) + i++ + p.endBackground() + p.bg = etree.NewElement("rect") + p.bg.CreateAttr("fill", palette[n]) + p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) + p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) + p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) + p.svg.InsertChildAt(0, p.bg) + } + case 2: + if i+3 < len(params) { + r := params[i+1].Param(0) + g := params[i+2].Param(0) + b := params[i+3].Param(0) + i += 3 + p.endBackground() + p.bg = etree.NewElement("rect") + p.bg.CreateAttr("fill", fmt.Sprintf("rgb(%d,%d,%d)", r, g, b)) + p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) + p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) + p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) + p.svg.InsertChildAt(0, p.bg) + } + } + } + } + i++ + } +} + +// processANSI processes ANSI escape sequences in the input text +func processANSI(input string, lines []*etree.Element, svg *etree.Element, config *Config, scale float64) { + d := newDispatcher(lines, svg, config, scale) + parser := ansi.NewParser() + parser.SetHandler(ansi.Handler{ + Print: d.Print, + HandleCsi: d.CsiDispatch, + Execute: d.Execute, + }) + + for _, line := range strings.Split(input, "\n") { + parser.Parse([]byte(line)) + d.Execute(ansi.LF) // simulate a newline + } +} + +// stripANSI removes ANSI escape sequences from text +func stripANSI(input string) string { + return ansi.Strip(input) +} + +// isANSI checks if the input contains ANSI escape sequences +func isANSI(input string) bool { + return stripANSI(input) != input +} + +// clamp constrains a value between min and max +func clamp(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +const fontHeightToWidthRatio = 1.68 + +// ANSI color palette +var ansiPalette = map[int]string{ + 30: "#000000", // black + 31: "#FF0000", // red + 32: "#00FF00", // green + 33: "#FFFF00", // yellow + 34: "#0000FF", // blue + 35: "#FF00FF", // magenta + 36: "#00FFFF", // cyan + 37: "#FFFFFF", // white + 90: "#808080", // bright black (gray) + 91: "#FF8080", // bright red + 92: "#80FF80", // bright green + 93: "#FFFF80", // bright yellow + 94: "#8080FF", // bright blue + 95: "#FF80FF", // bright magenta + 96: "#80FFFF", // bright cyan + 97: "#FFFFFF", // bright white +} + +// 256-color palette +var palette = []string{ + "#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0", + "#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + "#000000", "#00005f", "#000087", "#0000af", "#0000d7", "#0000ff", "#005f00", "#005f5f", + "#005f87", "#005faf", "#005fd7", "#005fff", "#008700", "#00875f", "#008787", "#0087af", + "#0087d7", "#0087ff", "#00af00", "#00af5f", "#00af87", "#00afaf", "#00afd7", "#00afff", + "#00d700", "#00d75f", "#00d787", "#00d7af", "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f", + "#00ff87", "#00ffaf", "#00ffd7", "#00ffff", "#5f0000", "#5f005f", "#5f0087", "#5f00af", + "#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", "#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff", + "#5f8700", "#5f875f", "#5f8787", "#5f87af", "#5f87d7", "#5f87ff", "#5faf00", "#5faf5f", + "#5faf87", "#5fafaf", "#5fafd7", "#5fafff", "#5fd700", "#5fd75f", "#5fd787", "#5fd7af", + "#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", "#5fff87", "#5fffaf", "#5fffd7", "#5fffff", + "#870000", "#87005f", "#870087", "#8700af", "#8700d7", "#8700ff", "#875f00", "#875f5f", + "#875f87", "#875faf", "#875fd7", "#875fff", "#878700", "#87875f", "#878787", "#8787af", + "#8787d7", "#8787ff", "#87af00", "#87af5f", "#87af87", "#87afaf", "#87afd7", "#87afff", + "#87d700", "#87d75f", "#87d787", "#87d7af", "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f", + "#87ff87", "#87ffaf", "#87ffd7", "#87ffff", "#af0000", "#af005f", "#af0087", "#af00af", + "#af00d7", "#af00ff", "#af5f00", "#af5f5f", "#af5f87", "#af5faf", "#af5fd7", "#af5fff", + "#af8700", "#af875f", "#af8787", "#af87af", "#af87d7", "#af87ff", "#afaf00", "#afaf5f", + "#afaf87", "#afafaf", "#afafd7", "#afafff", "#afd700", "#afd75f", "#afd787", "#afd7af", + "#afd7d7", "#afd7ff", "#afff00", "#afff5f", "#afff87", "#afffaf", "#afffd7", "#afffff", + "#d70000", "#d7005f", "#d70087", "#d700af", "#d700d7", "#d700ff", "#d75f00", "#d75f5f", + "#d75f87", "#d75faf", "#d75fd7", "#d75fff", "#d78700", "#d7875f", "#d78787", "#d787af", + "#d787d7", "#d787ff", "#d7af00", "#d7af5f", "#d7af87", "#d7afaf", "#d7afd7", "#d7afff", + "#d7d700", "#d7d75f", "#d7d787", "#d7d7af", "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f", + "#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", "#ff0000", "#ff005f", "#ff0087", "#ff00af", + "#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", "#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff", + "#ff8700", "#ff875f", "#ff8787", "#ff87af", "#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f", + "#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", "#ffd700", "#ffd75f", "#ffd787", "#ffd7af", + "#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", "#ffff87", "#ffffaf", "#ffffd7", "#ffffff", + "#080808", "#121212", "#1c1c1c", "#262626", "#303030", "#3a3a3a", "#444444", "#4e4e4e", + "#585858", "#626262", "#6c6c6c", "#767676", "#808080", "#8a8a8a", "#949494", "#9e9e9e", + "#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6", "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee", +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..b710e2d --- /dev/null +++ b/config.go @@ -0,0 +1,242 @@ +package freezelib + +import ( + "fmt" + "strconv" + "strings" +) + +// Config represents the configuration for generating code screenshots +type Config struct { + // Window settings + Background string `json:"background"` + Margin []float64 `json:"margin"` + Padding []float64 `json:"padding"` + Window bool `json:"window"` + Width float64 `json:"width"` + Height float64 `json:"height"` + + // Language and theme + Language string `json:"language"` + Theme string `json:"theme"` + Wrap int `json:"wrap"` + + // Decoration + Border Border `json:"border"` + Shadow Shadow `json:"shadow"` + + // Font + Font Font `json:"font"` + + // Line settings + LineHeight float64 `json:"line_height"` + Lines []int `json:"lines"` + ShowLineNumbers bool `json:"show_line_numbers"` +} + +// Shadow configuration for drop shadow effects +type Shadow struct { + Blur float64 `json:"blur"` + X float64 `json:"x"` + Y float64 `json:"y"` +} + +// Border configuration for window borders +type Border struct { + Radius float64 `json:"radius"` + Width float64 `json:"width"` + Color string `json:"color"` +} + +// Font configuration +type Font struct { + Family string `json:"family"` + File string `json:"file"` + Size float64 `json:"size"` + Ligatures bool `json:"ligatures"` +} + +// DefaultConfig returns a default configuration +func DefaultConfig() *Config { + return &Config{ + Background: "#171717", + Margin: []float64{0}, + Padding: []float64{20}, + Window: false, + Width: 0, + Height: 0, + Language: "", + Theme: "charm", + Wrap: 0, + Border: Border{Radius: 0, Width: 0, Color: "#515151"}, + Shadow: Shadow{Blur: 0, X: 0, Y: 0}, + Font: Font{Family: "JetBrains Mono", Size: 14, Ligatures: true}, + LineHeight: 1.2, + Lines: []int{}, + ShowLineNumbers: false, + } +} + +// SetPadding sets padding for all sides or specific sides +// Accepts 1, 2, or 4 values like CSS padding +func (c *Config) SetPadding(values ...float64) *Config { + c.Padding = values + return c +} + +// SetMargin sets margin for all sides or specific sides +// Accepts 1, 2, or 4 values like CSS margin +func (c *Config) SetMargin(values ...float64) *Config { + c.Margin = values + return c +} + +// SetFont sets font family and size +func (c *Config) SetFont(family string, size float64) *Config { + c.Font.Family = family + c.Font.Size = size + return c +} + +// SetTheme sets the syntax highlighting theme +func (c *Config) SetTheme(theme string) *Config { + c.Theme = theme + return c +} + +// SetLanguage sets the programming language for syntax highlighting +func (c *Config) SetLanguage(language string) *Config { + c.Language = language + return c +} + +// SetBackground sets the background color +func (c *Config) SetBackground(color string) *Config { + c.Background = color + return c +} + +// SetWindow enables or disables window controls +func (c *Config) SetWindow(enabled bool) *Config { + c.Window = enabled + return c +} + +// SetLineNumbers enables or disables line numbers +func (c *Config) SetLineNumbers(enabled bool) *Config { + c.ShowLineNumbers = enabled + return c +} + +// SetShadow sets shadow properties +func (c *Config) SetShadow(blur, x, y float64) *Config { + c.Shadow = Shadow{Blur: blur, X: x, Y: y} + return c +} + +// SetBorder sets border properties +func (c *Config) SetBorder(width, radius float64, color string) *Config { + c.Border = Border{Width: width, Radius: radius, Color: color} + return c +} + +// SetDimensions sets the output dimensions +func (c *Config) SetDimensions(width, height float64) *Config { + c.Width = width + c.Height = height + return c +} + +// SetLines sets the line range to capture (1-indexed) +func (c *Config) SetLines(start, end int) *Config { + if start > 0 && end > 0 && start <= end { + c.Lines = []int{start - 1, end - 1} // Convert to 0-indexed + } + return c +} + +// expandPadding expands padding values according to CSS rules +func (c *Config) expandPadding(scale float64) []float64 { + p := c.Padding + switch len(p) { + case 1: + return []float64{p[0] * scale, p[0] * scale, p[0] * scale, p[0] * scale} + case 2: + return []float64{p[0] * scale, p[1] * scale, p[0] * scale, p[1] * scale} + case 4: + return []float64{p[0] * scale, p[1] * scale, p[2] * scale, p[3] * scale} + default: + return []float64{0, 0, 0, 0} + } +} + +// expandMargin expands margin values according to CSS rules +func (c *Config) expandMargin(scale float64) []float64 { + m := c.Margin + switch len(m) { + case 1: + return []float64{m[0] * scale, m[0] * scale, m[0] * scale, m[0] * scale} + case 2: + return []float64{m[0] * scale, m[1] * scale, m[0] * scale, m[1] * scale} + case 4: + return []float64{m[0] * scale, m[1] * scale, m[2] * scale, m[3] * scale} + default: + return []float64{0, 0, 0, 0} + } +} + +// Clone creates a deep copy of the configuration +func (c *Config) Clone() *Config { + clone := *c + clone.Margin = make([]float64, len(c.Margin)) + copy(clone.Margin, c.Margin) + clone.Padding = make([]float64, len(c.Padding)) + copy(clone.Padding, c.Padding) + clone.Lines = make([]int, len(c.Lines)) + copy(clone.Lines, c.Lines) + return &clone +} + +// Validate checks if the configuration is valid +func (c *Config) Validate() error { + if c.Font.Size <= 0 { + return fmt.Errorf("font size must be positive") + } + if c.LineHeight <= 0 { + return fmt.Errorf("line height must be positive") + } + if len(c.Lines) == 2 && c.Lines[0] > c.Lines[1] { + return fmt.Errorf("start line must be less than or equal to end line") + } + return nil +} + +// parseColor validates and normalizes color values +func parseColor(color string) string { + color = strings.TrimSpace(color) + if color == "" { + return "#000000" + } + if !strings.HasPrefix(color, "#") { + color = "#" + color + } + return color +} + +// dimensionToInt converts dimension strings to integers +func dimensionToInt(dimension string) int { + dimension = strings.TrimSuffix(dimension, "px") + val, err := strconv.Atoi(dimension) + if err != nil { + return 0 + } + return val +} + +// side constants for padding/margin indexing +const ( + top = 0 + right = 1 + bottom = 2 + left = 3 +) diff --git a/examples/01-basic/main.go b/examples/01-basic/main.go new file mode 100644 index 0000000..1203d0e --- /dev/null +++ b/examples/01-basic/main.go @@ -0,0 +1,173 @@ +package main + +import ( + "fmt" + "github.com/landaiqing/freezelib" + "os" +) + +func main() { + fmt.Println("🎯 Basic Usage Examples") + fmt.Println("=======================") + + // Create output directory + os.MkdirAll("./output", 0755) + + // Run basic examples + simpleExample() + helloWorldExample() + quickStartExample() + defaultConfigExample() + + fmt.Println("\n✅ Basic examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") +} + +// Simple example - minimal code +func simpleExample() { + fmt.Println("\n📝 Simple Example") + fmt.Println("------------------") + + freeze := freezelib.New() + + code := `fmt.Println("Hello, FreezeLib!")` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/simple.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/simple.svg") +} + +// Hello World example - classic first program +func helloWorldExample() { + fmt.Println("\n👋 Hello World Example") + fmt.Println("-----------------------") + + freeze := freezelib.New() + + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") + fmt.Println("Welcome to FreezeLib!") + + // This is a comment + for i := 1; i <= 3; i++ { + fmt.Printf("Count: %d\n", i) + } +}` + + // Generate both SVG and PNG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error generating SVG: %v\n", err) + return + } + + pngData, err := freeze.GeneratePNGFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error generating PNG: %v\n", err) + return + } + + // Save files + os.WriteFile("output/hello_world.svg", svgData, 0644) + os.WriteFile("output/hello_world.png", pngData, 0644) + + fmt.Println("✅ Generated: output/hello_world.svg") + fmt.Println("✅ Generated: output/hello_world.png") +} + +// Quick start example - using QuickFreeze API +func quickStartExample() { + fmt.Println("\n⚡ Quick Start Example") + fmt.Println("----------------------") + + qf := freezelib.NewQuickFreeze() + + code := `function greet(name) { + return "Hello, " + name + "!"; +} + +const message = greet("FreezeLib"); +console.log(message); + +// Arrow function example +const multiply = (a, b) => a * b; +console.log("5 * 3 =", multiply(5, 3));` + + // Use QuickFreeze with basic styling + svgData, err := qf.WithTheme("github"). + WithFont("JetBrains Mono", 14). + WithLineNumbers(). + CodeToSVG(code) + + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/quick_start.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/quick_start.svg") +} + +// Default configuration example +func defaultConfigExample() { + fmt.Println("\n⚙️ Default Configuration Example") + fmt.Println("---------------------------------") + + // Show what default configuration looks like + config := freezelib.DefaultConfig() + freeze := freezelib.NewWithConfig(config) + + code := `# Python Example +def fibonacci(n): + """Calculate fibonacci number recursively.""" + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# Generate first 10 fibonacci numbers +print("Fibonacci sequence:") +for i in range(10): + print(f"F({i}) = {fibonacci(i)}")` + + svgData, err := freeze.GenerateFromCode(code, "python") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/default_config.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/default_config.svg") + + // Print configuration details + fmt.Println("\n📋 Default Configuration:") + fmt.Printf(" Theme: %s\n", config.Theme) + fmt.Printf(" Font: %s, %dpt\n", config.Font.Family, config.Font.Size) + fmt.Printf(" Background: %s\n", config.Background) + fmt.Printf(" Window: %t\n", config.Window) + fmt.Printf(" Line Numbers: %t\n", config.ShowLineNumbers) +} diff --git a/examples/02-formats/main.go b/examples/02-formats/main.go new file mode 100644 index 0000000..020f9c7 --- /dev/null +++ b/examples/02-formats/main.go @@ -0,0 +1,337 @@ +package main + +import ( + "fmt" + "github.com/landaiqing/freezelib" + "os" +) + +func main() { + fmt.Println("📊 Output Format Examples") + fmt.Println("=========================") + + // Create output directory + os.MkdirAll("output", 0755) + + // Run format examples + svgVsPngExample() + qualityComparisonExample() + dimensionExamples() + formatOptimizationExample() + + fmt.Println("\n✅ Format examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") + fmt.Println("📏 Compare file sizes and visual quality between formats.") +} + +// SVG vs PNG comparison +func svgVsPngExample() { + fmt.Println("\n🆚 SVG vs PNG Comparison") + fmt.Println("-------------------------") + + freeze := freezelib.New(). + WithTheme("github-dark"). + WithFont("JetBrains Mono", 14). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8) + + code := `package main + +import ( + "fmt" + "net/http" + "log" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) +} + +func main() { + http.HandleFunc("/", handler) + fmt.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +}` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error generating SVG: %v\n", err) + return + } + + // Generate PNG + pngData, err := freeze.GeneratePNGFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error generating PNG: %v\n", err) + return + } + + // Save files + svgPath := "output/comparison.svg" + pngPath := "output/comparison.png" + + os.WriteFile(svgPath, svgData, 0644) + os.WriteFile(pngPath, pngData, 0644) + + // Show file size comparison + svgInfo, _ := os.Stat(svgPath) + pngInfo, _ := os.Stat(pngPath) + + fmt.Printf("✅ Generated: %s (%d bytes)\n", svgPath, svgInfo.Size()) + fmt.Printf("✅ Generated: %s (%d bytes)\n", pngPath, pngInfo.Size()) + fmt.Printf("📊 Size ratio: PNG is %.1fx larger than SVG\n", + float64(pngInfo.Size())/float64(svgInfo.Size())) +} + +// Quality comparison with different settings +func qualityComparisonExample() { + fmt.Println("\n🎨 Quality Comparison") + fmt.Println("---------------------") + + baseCode := `def quicksort(arr): + if len(arr) <= 1: + return arr + + pivot = arr[len(arr) // 2] + left = [x for x in arr if x < pivot] + middle = [x for x in arr if x == pivot] + right = [x for x in arr if x > pivot] + + return quicksort(left) + middle + quicksort(right) + +# Example usage +numbers = [3, 6, 8, 10, 1, 2, 1] +sorted_numbers = quicksort(numbers) +print(f"Original: {numbers}") +print(f"Sorted: {sorted_numbers}")` + + // Different quality settings + configs := []struct { + name string + width float64 + height float64 + fontSize float64 + theme string + }{ + {"low_quality", 400, 300, 10, "github"}, + {"medium_quality", 800, 600, 14, "github-dark"}, + {"high_quality", 1200, 900, 16, "dracula"}, + {"ultra_quality", 1600, 1200, 18, "monokai"}, + } + + for _, config := range configs { + fmt.Printf("🔧 Generating %s...\n", config.name) + + freeze := freezelib.New(). + WithTheme(config.theme). + WithFont("JetBrains Mono", config.fontSize). + WithDimensions(config.width, config.height). + WithWindow(true). + WithLineNumbers(true). + WithShadow(10, 0, 5) + + // Generate PNG for quality comparison + pngData, err := freeze.GeneratePNGFromCode(baseCode, "python") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/quality_%s.png", config.name) + err = os.WriteFile(filename, pngData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + // Show file info + info, _ := os.Stat(filename) + fmt.Printf("✅ Generated: %s (%dx%d, %d bytes)\n", + filename, config.width, config.height, info.Size()) + } +} + +// Different dimension examples +func dimensionExamples() { + fmt.Println("\n📏 Dimension Examples") + fmt.Println("---------------------") + + code := `SELECT u.name, u.email, COUNT(o.id) as order_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.created_at >= '2024-01-01' +GROUP BY u.id, u.name, u.email +HAVING COUNT(o.id) > 0 +ORDER BY order_count DESC +LIMIT 10;` + + dimensions := []struct { + name string + width float64 + height float64 + desc string + }{ + {"square", 600, 600, "Square format"}, + {"wide", 1000, 400, "Wide format (presentations)"}, + {"tall", 400, 800, "Tall format (mobile)"}, + {"standard", 800, 600, "Standard 4:3 ratio"}, + {"widescreen", 1200, 675, "Widescreen 16:9 ratio"}, + } + + for _, dim := range dimensions { + fmt.Printf("📐 Creating %s format (%fx%f)...\n", dim.name, dim.width, dim.height) + + freeze := freezelib.New(). + WithTheme("nord"). + WithFont("Cascadia Code", 13). + WithDimensions(dim.width, dim.height). + WithWindow(true). + WithLineNumbers(true). + WithPadding(20) + + svgData, err := freeze.GenerateFromCode(code, "sql") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/dimension_%s.svg", dim.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, dim.desc) + } +} + +// Format optimization examples +func formatOptimizationExample() { + fmt.Println("\n⚡ Format Optimization") + fmt.Println("----------------------") + + code := `import React, { useState, useEffect } from 'react'; + +const TodoApp = () => { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(''); + + useEffect(() => { + // Load todos from localStorage + const saved = localStorage.getItem('todos'); + if (saved) { + setTodos(JSON.parse(saved)); + } + }, []); + + const addTodo = () => { + if (input.trim()) { + const newTodo = { + id: Date.now(), + text: input, + completed: false + }; + setTodos([...todos, newTodo]); + setInput(''); + } + }; + + return ( +
+

Todo List

+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ ); +}; + +export default TodoApp;` + + // Optimized for different use cases + optimizations := []struct { + name string + format string + description string + config func() *freezelib.Freeze + }{ + { + "web_optimized", + "svg", + "Optimized for web (small, scalable)", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("system-ui", 12). + WithPadding(15). + WithWindow(false) // No window for smaller size + }, + }, + { + "print_optimized", + "png", + "Optimized for print (high DPI)", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("Times New Roman", 14). + WithDimensions(1200, 900). + WithWindow(true). + WithLineNumbers(true). + WithShadow(0, 0, 0) // No shadow for print + }, + }, + { + "social_optimized", + "png", + "Optimized for social media", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("dracula"). + WithFont("Fira Code", 16). + WithDimensions(1080, 1080). // Square for Instagram + WithWindow(true). + WithShadow(20, 0, 15). + WithPadding(30) + }, + }, + } + + for _, opt := range optimizations { + fmt.Printf("🎯 Creating %s (%s)...\n", opt.name, opt.description) + + freeze := opt.config() + + var data []byte + var err error + var filename string + + if opt.format == "svg" { + data, err = freeze.GenerateFromCode(code, "javascript") + filename = fmt.Sprintf("output/optimized_%s.svg", opt.name) + } else { + data, err = freeze.GeneratePNGFromCode(code, "javascript") + filename = fmt.Sprintf("output/optimized_%s.png", opt.name) + } + + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + err = os.WriteFile(filename, data, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + info, _ := os.Stat(filename) + fmt.Printf("✅ Generated: %s (%d bytes)\n", filename, info.Size()) + } +} diff --git a/examples/03-themes/main.go b/examples/03-themes/main.go new file mode 100644 index 0000000..1cf8150 --- /dev/null +++ b/examples/03-themes/main.go @@ -0,0 +1,395 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("🎨 Theme Showcase Examples") + fmt.Println("===========================") + + // Create output directory + os.MkdirAll("output", 0755) + + // Run theme examples + popularThemesExample() + lightVsDarkExample() + themeComparisonExample() + customThemeExample() + + fmt.Println("\n✅ Theme examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") + fmt.Println("🎨 Compare different themes and their visual styles.") +} + +// Popular themes showcase +func popularThemesExample() { + fmt.Println("\n🌟 Popular Themes Showcase") + fmt.Println("--------------------------") + + code := `class DataProcessor: + def __init__(self, data_source): + self.data_source = data_source + self.processed_data = [] + + def process(self): + """Process the data with validation and transformation.""" + try: + raw_data = self.load_data() + validated_data = self.validate(raw_data) + self.processed_data = self.transform(validated_data) + return True + except Exception as e: + print(f"Processing failed: {e}") + return False + + def validate(self, data): + # Remove null values and duplicates + clean_data = [item for item in data if item is not None] + return list(set(clean_data)) + + def transform(self, data): + # Apply business logic transformations + return [item.upper() if isinstance(item, str) else item + for item in data]` + + // Popular themes to showcase + themes := []struct { + name string + description string + }{ + {"github", "GitHub light theme - clean and professional"}, + {"github-dark", "GitHub dark theme - modern and sleek"}, + {"dracula", "Dracula theme - purple and pink accents"}, + {"monokai", "Monokai theme - classic dark with vibrant colors"}, + {"solarized-dark", "Solarized dark - easy on the eyes"}, + {"solarized-light", "Solarized light - warm and readable"}, + {"nord", "Nord theme - arctic, north-bluish color palette"}, + {"one-dark", "One Dark theme - Atom's signature theme"}, + {"material", "Material theme - Google's material design"}, + {"vim", "Vim theme - classic terminal colors"}, + } + + for _, theme := range themes { + fmt.Printf("🎨 Generating %s theme...\n", theme.name) + + freeze := freezelib.New(). + WithTheme(theme.name). + WithFont("JetBrains Mono", 14). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(20) + + svgData, err := freeze.GenerateFromCode(code, "python") + if err != nil { + fmt.Printf("❌ Error with theme %s: %v\n", theme.name, err) + continue + } + + filename := fmt.Sprintf("output/theme_%s.svg", theme.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving %s: %v\n", filename, err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, theme.description) + } +} + +// Light vs Dark theme comparison +func lightVsDarkExample() { + fmt.Println("\n☀️🌙 Light vs Dark Comparison") + fmt.Println("------------------------------") + + code := ` + + + + + Modern Web App + + + +
+

Welcome to Our App

+
+

Features

+

This is a modern web application with responsive design.

+ +
+
+ +` + + // Light and dark theme pairs + themePairs := []struct { + light string + dark string + name string + }{ + {"github", "github-dark", "GitHub"}, + {"solarized-light", "solarized-dark", "Solarized"}, + {"material", "one-dark", "Material vs One Dark"}, + } + + for _, pair := range themePairs { + fmt.Printf("🔄 Comparing %s themes...\n", pair.name) + + // Light theme + lightFreeze := freezelib.New(). + WithTheme(pair.light). + WithFont("SF Mono", 13). + WithWindow(true). + WithLineNumbers(true). + WithShadow(10, 0, 5). + WithPadding(25) + + lightData, err := lightFreeze.GenerateFromCode(code, "html") + if err != nil { + fmt.Printf("❌ Error with light theme: %v\n", err) + continue + } + + // Dark theme + darkFreeze := freezelib.New(). + WithTheme(pair.dark). + WithFont("SF Mono", 13). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 10). + WithPadding(25) + + darkData, err := darkFreeze.GenerateFromCode(code, "html") + if err != nil { + fmt.Printf("❌ Error with dark theme: %v\n", err) + continue + } + + // Save files + lightFile := fmt.Sprintf("output/comparison_%s_light.svg", + sanitizeFilename(pair.name)) + darkFile := fmt.Sprintf("output/comparison_%s_dark.svg", + sanitizeFilename(pair.name)) + + os.WriteFile(lightFile, lightData, 0644) + os.WriteFile(darkFile, darkData, 0644) + + fmt.Printf("✅ Generated: %s (light)\n", lightFile) + fmt.Printf("✅ Generated: %s (dark)\n", darkFile) + } +} + +// Theme comparison grid +func themeComparisonExample() { + fmt.Println("\n📊 Theme Comparison Grid") + fmt.Println("------------------------") + + // Short code snippet for comparison + code := `fn main() { + let numbers = vec![1, 2, 3, 4, 5]; + + let doubled: Vec = numbers + .iter() + .map(|x| x * 2) + .collect(); + + println!("Original: {:?}", numbers); + println!("Doubled: {:?}", doubled); + + // Pattern matching + match doubled.len() { + 0 => println!("Empty vector"), + 1..=5 => println!("Small vector"), + _ => println!("Large vector"), + } +}` + + // Themes for comparison + comparisonThemes := []string{ + "github", "github-dark", "dracula", "monokai", + "nord", "one-dark", "material", "vim", + } + + for i, theme := range comparisonThemes { + fmt.Printf("🎯 Creating comparison sample %d: %s\n", i+1, theme) + + freeze := freezelib.New(). + WithTheme(theme). + WithFont("Fira Code", 12). + WithWindow(false). // No window for cleaner comparison + WithLineNumbers(false). + WithPadding(15). + WithDimensions(600, 400) // Consistent size + + svgData, err := freeze.GenerateFromCode(code, "rust") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/comparison_grid_%02d_%s.svg", i+1, theme) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s\n", filename) + } +} + +// Custom theme example +func customThemeExample() { + fmt.Println("\n🎨 Custom Theme Example") + fmt.Println("------------------------") + + code := `package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type Response struct { + Message string 'json:"message"' + Data map[string]interface{} 'json:"data"' + Status int 'json:"status"' +} + +func apiHandler(w http.ResponseWriter, r *http.Request) { + response := Response{ + Message: "API is working!", + Data: map[string]interface{}{ + "timestamp": "2024-01-15T10:30:00Z", + "version": "1.0.0", + "healthy": true, + }, + Status: 200, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func main() { + http.HandleFunc("/api/status", apiHandler) + fmt.Println("API server starting on :8080") + http.ListenAndServe(":8080", nil) +}` + + // Create custom-styled versions + customStyles := []struct { + name string + config func() *freezelib.Freeze + desc string + }{ + { + "corporate", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("Arial", 14). + WithBackground("#f8f9fa"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(5, 2, 3). + WithBorder(1, 4, "#dee2e6"). + WithPadding(30) + }, + "Corporate style - clean and professional", + }, + { + "cyberpunk", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("dracula"). + WithFont("Courier New", 13). + WithBackground("#0d1117"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(20, 0, 15). + WithBorder(2, 0, "#ff79c6"). + WithPadding(25) + }, + "Cyberpunk style - neon and futuristic", + }, + { + "minimal", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("system-ui", 13). + WithBackground("#ffffff"). + WithWindow(false). + WithLineNumbers(false). + WithShadow(0, 0, 0). + WithPadding(20) + }, + "Minimal style - clean and distraction-free", + }, + } + + for _, style := range customStyles { + fmt.Printf("✨ Creating %s style...\n", style.name) + + freeze := style.config() + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/custom_%s.svg", style.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, style.desc) + } +} + +// Helper function to sanitize filenames +func sanitizeFilename(name string) string { + // Replace spaces and special characters with underscores + result := "" + for _, char := range name { + if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') { + result += string(char) + } else { + result += "_" + } + } + return result +} diff --git a/examples/04-languages/main.go b/examples/04-languages/main.go new file mode 100644 index 0000000..de5a774 --- /dev/null +++ b/examples/04-languages/main.go @@ -0,0 +1,896 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("💻 Programming Languages Examples") + fmt.Println("=================================") + + // Create output directory + os.MkdirAll("output", 0755) + + // Run language examples + popularLanguagesExample() + languageComparisonExample() + multiLanguageProjectExample() + languageSpecificFeaturesExample() + + fmt.Println("\n✅ Language examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") +} + +// Popular programming languages showcase +func popularLanguagesExample() { + fmt.Println("\n🌟 Popular Languages Showcase") + fmt.Println("-----------------------------") + + // Language examples with sample code + languages := []struct { + name string + code string + lang string + }{ + { + "go", + `package main + +import ( + "fmt" + "time" +) + +func main() { + fmt.Println("Hello from Go!") + + // Goroutine example + go func() { + fmt.Println("This runs concurrently") + }() + + time.Sleep(time.Millisecond * 100) +}`, + "go", + }, + { + "python", + `import asyncio +from dataclasses import dataclass + +@dataclass +class User: + name: str + age: int + email: str + +async def fetch_user(user_id: int) -> User: + # Simulate API call + await asyncio.sleep(1) + return User(name="John Doe", age=30, email="john@example.com") + +async def main(): + user = await fetch_user(123) + print(f"User: {user.name}, {user.age}, {user.email}") + +if __name__ == "__main__": + asyncio.run(main())`, + "python", + }, + { + "javascript", + `// Modern JavaScript example +const fetchData = async () => { + try { + const response = await fetch('https://api.example.com/data'); + const data = await response.json(); + + // Destructuring and spread operator + const { items, meta } = data; + const allItems = [...items, { id: 'new', value: 42 }]; + + // Array methods + const filtered = allItems.filter(item => item.value > 10); + + return filtered; + } catch (error) { + console.error('Error fetching data:', error); + return []; + } +}; + +// Call the function +fetchData().then(result => console.log(result));`, + "javascript", + }, + { + "rust", + `use std::collections::HashMap; + +#[derive(Debug)] +struct User { + name: String, + age: u32, + active: bool, +} + +fn main() { + // Create a new User + let user = User { + name: String::from("Alice"), + age: 28, + active: true, + }; + + // Using a HashMap + let mut scores = HashMap::new(); + scores.insert(String::from("Blue"), 10); + scores.insert(String::from("Yellow"), 50); + + // Pattern matching + match user.age { + 0..=17 => println!("{} is underage", user.name), + 18..=64 => println!("{} is an adult", user.name), + _ => println!("{} is a senior", user.name), + } +}`, + "rust", + }, + { + "java", + `import java.util.List; +import java.util.stream.Collectors; + +public class StreamExample { + public static void main(String[] args) { + // Create a list of names + List names = List.of("Alice", "Bob", "Charlie", "David"); + + // Use streams to filter and transform + List filteredNames = names.stream() + .filter(name -> name.length() > 4) + .map(String::toUpperCase) + .sorted() + .collect(Collectors.toList()); + + // Print the results + System.out.println("Original names: " + names); + System.out.println("Filtered names: " + filteredNames); + } +}`, + "java", + }, + { + "csharp", + `using System; +using System.Collections.Generic; +using System.Linq; + +namespace LinqExample +{ + class Program + { + static void Main(string[] args) + { + // Create a list of products + var products = new List + { + new Product { Id = 1, Name = "Laptop", Price = 1200.00m, Category = "Electronics" }, + new Product { Id = 2, Name = "Desk Chair", Price = 250.50m, Category = "Furniture" }, + new Product { Id = 3, Name = "Coffee Maker", Price = 89.99m, Category = "Kitchen" }, + new Product { Id = 4, Name = "Tablet", Price = 400.00m, Category = "Electronics" } + }; + + // Use LINQ to query products + var expensiveElectronics = products + .Where(p => p.Category == "Electronics" && p.Price > 500) + .OrderBy(p => p.Price) + .Select(p => new { p.Name, p.Price }); + + Console.WriteLine("Expensive Electronics:"); + foreach (var item in expensiveElectronics) + { + Console.WriteLine($"{item.Name}: ${item.Price}"); + } + } + } + + class Product + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public string Category { get; set; } + } +}`, + "csharp", + }, + } + + // Create a consistent style for all languages + freeze := freezelib.New(). + WithTheme("github-dark"). + WithFont("JetBrains Mono", 14). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(20) + + for _, lang := range languages { + fmt.Printf("📝 Generating %s example...\n", lang.name) + + svgData, err := freeze.GenerateFromCode(lang.code, lang.lang) + if err != nil { + fmt.Printf("❌ Error with %s: %v\n", lang.name, err) + continue + } + + filename := fmt.Sprintf("output/language_%s.svg", lang.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 comparison with the same algorithm +func languageComparisonExample() { + fmt.Println("\n🔄 Same Algorithm in Different Languages") + fmt.Println("---------------------------------------") + + // Fibonacci implementation in different languages + fibImplementations := []struct { + name string + code string + lang string + }{ + { + "go", + `package main + +import "fmt" + +// Recursive Fibonacci implementation +func fibonacci(n int) int { + if n <= 1 { + return n + } + return fibonacci(n-1) + fibonacci(n-2) +} + +// Iterative Fibonacci implementation +func fibonacciIterative(n int) int { + if n <= 1 { + return n + } + + a, b := 0, 1 + for i := 2; i <= n; i++ { + a, b = b, a+b + } + return b +} + +func main() { + n := 10 + fmt.Printf("Fibonacci(%d) = %d (recursive)\n", n, fibonacci(n)) + fmt.Printf("Fibonacci(%d) = %d (iterative)\n", n, fibonacciIterative(n)) +}`, + "go", + }, + { + "python", + `def fibonacci_recursive(n): + """Calculate Fibonacci number recursively.""" + if n <= 1: + return n + return fibonacci_recursive(n-1) + fibonacci_recursive(n-2) + +def fibonacci_iterative(n): + """Calculate Fibonacci number iteratively.""" + if n <= 1: + return n + + a, b = 0, 1 + for i in range(2, n+1): + a, b = b, a + b + return b + +def fibonacci_dynamic(n): + """Calculate Fibonacci number using dynamic programming.""" + memo = {0: 0, 1: 1} + + def fib(n): + if n not in memo: + memo[n] = fib(n-1) + fib(n-2) + return memo[n] + + return fib(n) + +# Test the functions +n = 10 +print(f"Fibonacci({n}) = {fibonacci_recursive(n)} (recursive)") +print(f"Fibonacci({n}) = {fibonacci_iterative(n)} (iterative)") +print(f"Fibonacci({n}) = {fibonacci_dynamic(n)} (dynamic)")`, + "python", + }, + { + "javascript", + `// Recursive Fibonacci implementation +function fibonacciRecursive(n) { + if (n <= 1) return n; + return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2); +} + +// Iterative Fibonacci implementation +function fibonacciIterative(n) { + if (n <= 1) return n; + + let a = 0, b = 1; + for (let i = 2; i <= n; i++) { + const temp = a + b; + a = b; + b = temp; + } + return b; +} + +// Fibonacci with memoization +function fibonacciMemoized(n, memo = {}) { + if (n in memo) return memo[n]; + if (n <= 1) return n; + + memo[n] = fibonacciMemoized(n - 1, memo) + fibonacciMemoized(n - 2, memo); + return memo[n]; +} + +// Test the functions +const n = 10; +console.log(Fibonacci(${n}) = ${fibonacciRecursive(n)} (recursive)); +console.log(Fibonacci(${n}) = ${fibonacciIterative(n)} (iterative)); +console.log(Fibonacci(${n}) = ${fibonacciMemoized(n)} (memoized));`, + "javascript", + }, + { + "rust", + `fn fibonacci_recursive(n: u32) -> u32 { + match n { + 0 => 0, + 1 => 1, + _ => fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2), + } +} + +fn fibonacci_iterative(n: u32) -> u32 { + match n { + 0 => 0, + 1 => 1, + _ => { + let mut a = 0; + let mut b = 1; + + for _ in 2..=n { + let temp = a + b; + a = b; + b = temp; + } + + b + } + } +} + +fn main() { + let n = 10; + println!("Fibonacci({}) = {} (recursive)", n, fibonacci_recursive(n)); + println!("Fibonacci({}) = {} (iterative)", n, fibonacci_iterative(n)); +}`, + "rust", + }, + } + + // Create a consistent style for comparison + freeze := freezelib.New(). + WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(true). + WithLineNumbers(true). + WithPadding(20) + + for _, impl := range fibImplementations { + fmt.Printf("🧮 Generating Fibonacci in %s...\n", impl.name) + + svgData, err := freeze.GenerateFromCode(impl.code, impl.lang) + if err != nil { + fmt.Printf("❌ Error with %s: %v\n", impl.name, err) + continue + } + + filename := fmt.Sprintf("output/fibonacci_%s.svg", impl.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) + } +} + +// Multi-language project example +func multiLanguageProjectExample() { + fmt.Println("\n🌐 Multi-language Project") + fmt.Println("-------------------------") + + // Different files in a web project + projectFiles := []struct { + name string + code string + lang string + filename string + }{ + { + "HTML", + ` + + + + + Task Manager + + + +
+

Task Manager

+ +
+ + +
+ +
    + +
    +

    Total tasks: 0

    +

    Completed: 0

    +
    +
    + + + +`, + "html", + "index.html", + }, + { + "CSS", + `/* Task Manager Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + background-color: #f5f5f5; + color: #333; +} + +.container { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +h1 { + text-align: center; + margin-bottom: 2rem; + color: #2c3e50; +} + +.task-form { + display: flex; + margin-bottom: 1.5rem; +} + +#taskInput { + flex: 1; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px 0 0 4px; + font-size: 1rem; +} + +#addTask { + padding: 0.75rem 1.5rem; + background-color: #3498db; + color: white; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; + font-size: 1rem; +} + +.task-list { + list-style: none; + margin-bottom: 1.5rem; +} + +.task-list li { + padding: 1rem; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; +} + +.task-list li.completed { + text-decoration: line-through; + color: #7f8c8d; +} + +.stats { + display: flex; + justify-content: space-between; + color: #7f8c8d; + font-size: 0.9rem; +}`, + "css", + "styles.css", + }, + { + "JavaScript", + `// Task Manager App +document.addEventListener('DOMContentLoaded', () => { + // DOM Elements + const taskInput = document.getElementById('taskInput'); + const addTaskBtn = document.getElementById('addTask'); + const taskList = document.getElementById('taskList'); + const totalTasksEl = document.getElementById('totalTasks'); + const completedTasksEl = document.getElementById('completedTasks'); + + // Task array + let tasks = JSON.parse(localStorage.getItem('tasks')) || []; + + // Initial render + renderTasks(); + updateStats(); + + // Event Listeners + addTaskBtn.addEventListener('click', addTask); + taskInput.addEventListener('keypress', e => { + if (e.key === 'Enter') addTask(); + }); + + // Add a new task + function addTask() { + const taskText = taskInput.value.trim(); + if (taskText === '') return; + + tasks.push({ + id: Date.now(), + text: taskText, + completed: false + }); + + saveToLocalStorage(); + renderTasks(); + updateStats(); + + taskInput.value = ''; + taskInput.focus(); + } + + // Toggle task completion + function toggleTask(id) { + tasks = tasks.map(task => + task.id === id ? { ...task, completed: !task.completed } : task + ); + + saveToLocalStorage(); + renderTasks(); + updateStats(); + } + + // Delete a task + function deleteTask(id) { + tasks = tasks.filter(task => task.id !== id); + + saveToLocalStorage(); + renderTasks(); + updateStats(); + } + + // Render tasks to DOM + function renderTasks() { + taskList.innerHTML = ''; + + tasks.forEach(task => { + const li = document.createElement('li'); + li.className = task.completed ? 'completed' : ''; + + li.innerHTML = + ${task.text} + + ; + + taskList.appendChild(li); + }); + } + + // Update statistics + function updateStats() { + totalTasksEl.textContent = tasks.length; + completedTasksEl.textContent = tasks.filter(task => task.completed).length; + } + + // Save to localStorage + function saveToLocalStorage() { + localStorage.setItem('tasks', JSON.stringify(tasks)); + } + + // Expose functions to global scope for inline event handlers + window.toggleTask = toggleTask; + window.deleteTask = deleteTask; +});`, + "javascript", + "app.js", + }, + } + + // Create a consistent style for the project files + freeze := freezelib.New(). + WithTheme("github"). + WithFont("Cascadia Code", 13). + WithWindow(true). + WithLineNumbers(true). + WithShadow(10, 0, 5). + WithPadding(20) + + for _, file := range projectFiles { + fmt.Printf("📄 Generating %s file (%s)...\n", file.name, file.filename) + + svgData, err := freeze.GenerateFromCode(file.code, file.lang) + if err != nil { + fmt.Printf("❌ Error with %s: %v\n", file.name, err) + continue + } + + filename := fmt.Sprintf("output/project_%s.svg", file.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-specific features example +func languageSpecificFeaturesExample() { + fmt.Println("\n✨ Language-Specific Features") + fmt.Println("----------------------------") + + // Language-specific code features + features := []struct { + name string + code string + lang string + description string + }{ + { + "go_concurrency", + `package main + +import ( + "fmt" + "sync" + "time" +) + +func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) { + defer wg.Done() + + for j := range jobs { + fmt.Printf("Worker %d started job %d\n", id, j) + time.Sleep(time.Second) // Simulate work + fmt.Printf("Worker %d finished job %d\n", id, j) + results <- j * 2 + } +} + +func main() { + jobs := make(chan int, 5) + results := make(chan int, 5) + + // Start workers + var wg sync.WaitGroup + for w := 1; w <= 3; w++ { + wg.Add(1) + go worker(w, jobs, results, &wg) + } + + // Send jobs + for j := 1; j <= 5; j++ { + jobs <- j + } + close(jobs) + + // Wait for workers to finish + wg.Wait() + close(results) + + // Collect results + for r := range results { + fmt.Println("Result:", r) + } +}`, + "go", + "Go Concurrency with Goroutines and Channels", + }, + { + "python_decorators", + `import time +import functools +from typing import Callable, TypeVar, Any + +T = TypeVar('T') + +def timer(func: Callable[..., T]) -> Callable[..., T]: + """Decorator that prints the execution time of a function.""" + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds") + return result + return wrapper + +def memoize(func: Callable[..., T]) -> Callable[..., T]: + """Decorator that caches function results.""" + cache = {} + + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + key = str(args) + str(kwargs) + if key not in cache: + cache[key] = func(*args, **kwargs) + return cache[key] + return wrapper + +@timer +@memoize +def fibonacci(n: int) -> int: + """Calculate the nth Fibonacci number.""" + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# Test the decorated function +print(fibonacci(30)) # First call will be timed +print(fibonacci(30)) # Second call will use cached result`, + "python", + "Python Decorators and Type Annotations", + }, + { + "typescript_generics", + `// TypeScript Generics and Interfaces +interface Repository { + getById(id: string): Promise; + getAll(): Promise; + create(item: T): Promise; + update(id: string, item: T): Promise; + delete(id: string): Promise; +} + +interface User { + id?: string; + name: string; + email: string; + role: 'admin' | 'user' | 'guest'; + createdAt?: Date; +} + +class UserRepository implements Repository { + private users: Map = new Map(); + + async getById(id: string): Promise { + const user = this.users.get(id); + if (!user) { + throw new Error(User with id ${id} not found); + } + return user; + } + + async getAll(): Promise { + return Array.from(this.users.values()); + } + + async create(user: User): Promise { + const id = Math.random().toString(36).substring(2, 9); + const newUser = { + ...user, + id, + createdAt: new Date() + }; + this.users.set(id, newUser); + return newUser; + } + + async update(id: string, user: User): Promise { + if (!this.users.has(id)) { + throw new Error('User with id ${id} not found'); + } + const updatedUser = { ...user, id }; + this.users.set(id, updatedUser); + return updatedUser; + } + + async delete(id: string): Promise { + return this.users.delete(id); + } +} + +// Usage example +async function main() { + const userRepo = new UserRepository(); + + const newUser = await userRepo.create({ + name: 'John Doe', + email: 'john@example.com', + role: 'admin' + }); + + console.log('Created user:', newUser); + + const allUsers = await userRepo.getAll(); + console.log('All users:', allUsers); +} + +main().catch(console.error);`, + "typescript", + "TypeScript Generics, Interfaces and Type Safety", + }, + } + + // Create a consistent style for the feature examples + freeze := freezelib.New(). + WithTheme("one-dark"). + WithFont("JetBrains Mono", 13). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(20) + + for _, feature := range features { + fmt.Printf("🔍 Generating %s example...\n", feature.name) + + svgData, err := freeze.GenerateFromCode(feature.code, feature.lang) + if err != nil { + fmt.Printf("❌ Error with %s: %v\n", feature.name, err) + continue + } + + filename := fmt.Sprintf("output/feature_%s.svg", feature.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving %s: %v\n", filename, err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, feature.description) + } +} diff --git a/examples/05-terminal/main.go b/examples/05-terminal/main.go new file mode 100644 index 0000000..2f5543f --- /dev/null +++ b/examples/05-terminal/main.go @@ -0,0 +1,411 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("💻 Terminal Output Examples") + fmt.Println("============================") + + // Create output directory + os.MkdirAll("output", 0755) + + // Run terminal examples + basicAnsiExample() + buildOutputExample() + testResultsExample() + dockerOutputExample() + gitOutputExample() + systemLogsExample() + + fmt.Println("\n✅ Terminal examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") +} + +// Basic ANSI color example +func basicAnsiExample() { + fmt.Println("\n🌈 Basic ANSI Colors") + fmt.Println("--------------------") + + // Terminal preset optimized for ANSI output + freeze := freezelib.NewWithPreset("terminal") + + ansiOutput := `\033[32m✓ SUCCESS\033[0m: Application started successfully +\033[33m⚠ WARNING\033[0m: Configuration file not found, using defaults +\033[31m✗ ERROR\033[0m: Failed to connect to database +\033[36mINFO\033[0m: Server listening on port 8080 +\033[35mDEBUG\033[0m: Loading user preferences +\033[37mTRACE\033[0m: Function call: getUserById(123) + +\033[1mBold text\033[0m and \033[4munderlined text\033[0m +\033[7mReversed text\033[0m and \033[9mstrikethrough text\033[0m + +Background colors: +\033[41mRed background\033[0m +\033[42mGreen background\033[0m +\033[43mYellow background\033[0m +\033[44mBlue background\033[0m` + + svgData, err := freeze.GenerateFromANSI(ansiOutput) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/basic_ansi.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/basic_ansi.svg") +} + +// Build output example +func buildOutputExample() { + fmt.Println("\n🔨 Build Output") + fmt.Println("---------------") + + freeze := freezelib.New(). + WithTheme("github-dark"). + WithFont("Cascadia Code", 13). + WithWindow(true). + WithPadding(20). + WithBackground("#0d1117") + + buildOutput := `$ go build -v ./... +github.com/myproject/internal/config +github.com/myproject/internal/database +github.com/myproject/internal/handlers +github.com/myproject/cmd/server + +\033[32m✓ Build completed successfully\033[0m + +$ go test -v ./... +=== RUN TestUserService_CreateUser +--- PASS: TestUserService_CreateUser (0.01s) +=== RUN TestUserService_GetUser +--- PASS: TestUserService_GetUser (0.00s) +=== RUN TestUserService_UpdateUser +--- PASS: TestUserService_UpdateUser (0.01s) +=== RUN TestUserService_DeleteUser +--- PASS: TestUserService_DeleteUser (0.00s) +=== RUN TestDatabaseConnection +--- PASS: TestDatabaseConnection (0.05s) + +\033[32mPASS\033[0m +\033[32mok \033[0m github.com/myproject 0.123s + +\033[36mCoverage: 85.7% of statements\033[0m + +$ docker build -t myapp:latest . +Sending build context to Docker daemon 2.048kB +Step 1/8 : FROM golang:1.21-alpine AS builder + ---> 7642119cd161 +Step 2/8 : WORKDIR /app + ---> Using cache + ---> 8f3b8c9d4e5f +Step 3/8 : COPY go.mod go.sum ./ + ---> Using cache + ---> 1a2b3c4d5e6f +Step 4/8 : RUN go mod download + ---> Using cache + ---> 2b3c4d5e6f7g +Step 5/8 : COPY . . + ---> 3c4d5e6f7g8h +Step 6/8 : RUN go build -o main . + ---> Running in 4d5e6f7g8h9i + ---> 5e6f7g8h9i0j +Step 7/8 : FROM alpine:latest + ---> 6f7g8h9i0j1k +Step 8/8 : COPY --from=builder /app/main /main + ---> 7g8h9i0j1k2l +\033[32mSuccessfully built 7g8h9i0j1k2l\033[0m +\033[32mSuccessfully tagged myapp:latest\033[0m` + + svgData, err := freeze.GenerateFromANSI(buildOutput) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/build_output.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/build_output.svg") +} + +// Test results example +func testResultsExample() { + fmt.Println("\n🧪 Test Results") + fmt.Println("---------------") + + freeze := freezelib.New(). + WithTheme("dracula"). + WithFont("JetBrains Mono", 14). + WithWindow(true). + WithLineNumbers(false). + WithShadow(15, 0, 10). + WithPadding(25) + + testOutput := `$ npm test + +> myapp@1.0.0 test +> jest --coverage + + PASS src/components/Button.test.js + Button Component + \033[32m✓\033[0m renders correctly (15ms) + \033[32m✓\033[0m handles click events (8ms) + \033[32m✓\033[0m applies custom className (3ms) + + PASS src/services/api.test.js + API Service + \033[32m✓\033[0m fetches user data (45ms) + \033[32m✓\033[0m handles network errors (12ms) + \033[32m✓\033[0m retries failed requests (23ms) + + FAIL src/utils/validation.test.js + Validation Utils + \033[32m✓\033[0m validates email addresses (5ms) + \033[31m✗\033[0m validates phone numbers (8ms) + \033[32m✓\033[0m validates passwords (3ms) + + ● Validation Utils › validates phone numbers + + expect(received).toBe(expected) + + Expected: true + Received: false + + 12 | test('validates phone numbers', () => { + 13 | const phoneNumber = '+1-555-123-4567'; + > 14 | expect(isValidPhoneNumber(phoneNumber)).toBe(true); + | ^ + 15 | }); + + at Object. (src/utils/validation.test.js:14:45) + +\033[33mTest Suites: 1 failed, 2 passed, 3 total\033[0m +\033[33mTests: 1 failed, 7 passed, 8 total\033[0m +\033[33mSnapshots: 0 total\033[0m +\033[33mTime: 2.847s\033[0m + +\033[36m----------------------|---------|----------|---------|---------|-------------------\033[0m +\033[36mFile | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s\033[0m +\033[36m----------------------|---------|----------|---------|---------|-------------------\033[0m +\033[36mAll files | 87.5 | 75.0 | 90.0 | 87.5 |\033[0m +\033[36m src/components | 95.0 | 85.0 | 100.0 | 95.0 |\033[0m +\033[36m Button.js | 95.0 | 85.0 | 100.0 | 95.0 | 23\033[0m +\033[36m src/services | 80.0 | 65.0 | 80.0 | 80.0 |\033[0m +\033[36m api.js | 80.0 | 65.0 | 80.0 | 80.0 | 45,67\033[0m +\033[36m src/utils | 87.5 | 75.0 | 90.0 | 87.5 |\033[0m +\033[36m validation.js | 87.5 | 75.0 | 90.0 | 87.5 | 34\033[0m +\033[36m----------------------|---------|----------|---------|---------|-------------------\033[0m` + + svgData, err := freeze.GenerateFromANSI(testOutput) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/test_results.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/test_results.svg") +} + +// Docker output example +func dockerOutputExample() { + fmt.Println("\n🐳 Docker Output") + fmt.Println("----------------") + + freeze := freezelib.New(). + WithTheme("nord"). + WithFont("SF Mono", 13). + WithWindow(true). + WithPadding(20). + WithBackground("#2e3440") + + dockerOutput := `$ docker-compose up -d +Creating network "myapp_default" with the default driver +Creating volume "myapp_postgres_data" with default driver +Creating volume "myapp_redis_data" with default driver + +\033[33mPulling postgres (postgres:13)...\033[0m +13: Pulling from library/postgres +\033[36m7b1a6ab2e44d\033[0m: Pull complete +\033[36m5c9d4e5f6a7b\033[0m: Pull complete +\033[36m8c1d2e3f4a5b\033[0m: Pull complete +\033[36m9d2e3f4a5b6c\033[0m: Pull complete +\033[32mDigest: sha256:abc123def456...\033[0m +\033[32mStatus: Downloaded newer image for postgres:13\033[0m + +\033[33mPulling redis (redis:6-alpine)...\033[0m +6-alpine: Pulling from library/redis +\033[36m4c0d5e6f7a8b\033[0m: Pull complete +\033[36m5d1e6f7a8b9c\033[0m: Pull complete +\033[32mDigest: sha256:def456ghi789...\033[0m +\033[32mStatus: Downloaded newer image for redis:6-alpine\033[0m + +Creating myapp_postgres_1 ... \033[32mdone\033[0m +Creating myapp_redis_1 ... \033[32mdone\033[0m +Creating myapp_web_1 ... \033[32mdone\033[0m + +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +\033[36m1a2b3c4d5e6f\033[0m myapp:latest "go run main.go" 2 minutes ago Up 2 minutes \033[35m0.0.0.0:8080->8080/tcp\033[0m myapp_web_1 +\033[36m2b3c4d5e6f7g\033[0m postgres:13 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes \033[35m5432/tcp\033[0m myapp_postgres_1 +\033[36m3c4d5e6f7g8h\033[0m redis:6-alpine "docker-entrypoint.s…" 2 minutes ago Up 2 minutes \033[35m6379/tcp\033[0m myapp_redis_1 + +$ docker logs myapp_web_1 +\033[36m2024/01/15 10:30:00\033[0m \033[32mINFO\033[0m Starting server... +\033[36m2024/01/15 10:30:00\033[0m \033[32mINFO\033[0m Connected to database +\033[36m2024/01/15 10:30:00\033[0m \033[32mINFO\033[0m Connected to Redis +\033[36m2024/01/15 10:30:00\033[0m \033[32mINFO\033[0m Server listening on :8080 +\033[36m2024/01/15 10:30:15\033[0m \033[36mDEBUG\033[0m GET /api/health - 200 OK (2ms) +\033[36m2024/01/15 10:30:20\033[0m \033[36mDEBUG\033[0m POST /api/users - 201 Created (45ms)` + + svgData, err := freeze.GenerateFromANSI(dockerOutput) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/docker_output.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/docker_output.svg") +} + +// Git output example +func gitOutputExample() { + fmt.Println("\n📚 Git Output") + fmt.Println("-------------") + + freeze := freezelib.New(). + WithTheme("github"). + WithFont("Menlo", 13). + WithWindow(true). + WithPadding(20). + WithBackground("#ffffff") + + gitOutput := `$ git status +On branch feature/user-authentication +Your branch is ahead of 'origin/main' by 3 commits. + (use "git push" to publish your local commits) + +Changes to be committed: + (use "git restore --staged ..." to unstage) + \033[32mnew file: src/auth/login.js\033[0m + \033[32mnew file: src/auth/register.js\033[0m + \033[32mmodified: src/app.js\033[0m + \033[32mmodified: package.json\033[0m + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + \033[31mmodified: README.md\033[0m + \033[31mmodified: src/components/Header.js\033[0m + +Untracked files: + (use "git add ..." to include in what will be committed) + \033[31msrc/auth/middleware.js\033[0m + \033[31mtests/auth.test.js\033[0m + +$ git log --oneline -5 +\033[33m7a8b9c0\033[0m \033[32m(HEAD -> feature/user-authentication)\033[0m Add user registration functionality +\033[33m6a7b8c9\033[0m Add login form validation +\033[33m5a6b7c8\033[0m Implement JWT authentication +\033[33m4a5b6c7\033[0m \033[36m(origin/main, main)\033[0m Update project dependencies +\033[33m3a4b5c6\033[0m Fix responsive design issues + +$ git diff --stat + README.md | 15 \033[32m+++++++++\033[0m\033[31m------\033[0m + package.json | 3 \033[32m+++\033[0m + src/app.js | 42 \033[32m++++++++++++++++++++++++++++++\033[0m\033[31m----------\033[0m + src/auth/login.js | 67 \033[32m+++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m + src/auth/register.js | 89 \033[32m++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m + src/components/Header.js | 8 \033[32m+++++\033[0m\033[31m---\033[0m + 6 files changed, 201 insertions(+), 23 deletions(-)` + + svgData, err := freeze.GenerateFromANSI(gitOutput) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/git_output.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/git_output.svg") +} + +// System logs example +func systemLogsExample() { + fmt.Println("\n📋 System Logs") + fmt.Println("--------------") + + freeze := freezelib.New(). + WithTheme("monokai"). + WithFont("Ubuntu Mono", 13). + WithWindow(true). + WithPadding(20). + WithBackground("#272822") + + systemLogs := `$ tail -f /var/log/application.log + +\033[90m2024-01-15 10:30:00.123\033[0m [\033[32mINFO \033[0m] \033[36mApplication\033[0m - Server starting up +\033[90m2024-01-15 10:30:00.456\033[0m [\033[32mINFO \033[0m] \033[36mDatabase \033[0m - Connection pool initialized (size: 10) +\033[90m2024-01-15 10:30:00.789\033[0m [\033[32mINFO \033[0m] \033[36mCache \033[0m - Redis connection established +\033[90m2024-01-15 10:30:01.012\033[0m [\033[32mINFO \033[0m] \033[36mSecurity \033[0m - JWT secret loaded from environment +\033[90m2024-01-15 10:30:01.234\033[0m [\033[32mINFO \033[0m] \033[36mHTTP \033[0m - Server listening on port 8080 + +\033[90m2024-01-15 10:30:15.567\033[0m [\033[34mDEBUG\033[0m] \033[36mAuth \033[0m - User login attempt: user@example.com +\033[90m2024-01-15 10:30:15.678\033[0m [\033[32mINFO \033[0m] \033[36mAuth \033[0m - User authenticated successfully: user@example.com +\033[90m2024-01-15 10:30:15.789\033[0m [\033[34mDEBUG\033[0m] \033[36mHTTP \033[0m - POST /api/auth/login - 200 OK (223ms) + +\033[90m2024-01-15 10:30:30.123\033[0m [\033[33mWARN \033[0m] \033[36mDatabase \033[0m - Slow query detected (1.2s): SELECT * FROM users WHERE... +\033[90m2024-01-15 10:30:30.234\033[0m [\033[34mDEBUG\033[0m] \033[36mHTTP \033[0m - GET /api/users - 200 OK (1234ms) + +\033[90m2024-01-15 10:30:45.456\033[0m [\033[31mERROR\033[0m] \033[36mPayment \033[0m - Payment processing failed: insufficient funds +\033[90m2024-01-15 10:30:45.567\033[0m [\033[31mERROR\033[0m] \033[36mPayment \033[0m - Stack trace: + at PaymentService.processPayment (payment.js:45:12) + at OrderController.createOrder (order.js:23:8) + at Router.handle (express.js:123:5) +\033[90m2024-01-15 10:30:45.678\033[0m [\033[34mDEBUG\033[0m] \033[36mHTTP \033[0m - POST /api/orders - 400 Bad Request (112ms) + +\033[90m2024-01-15 10:31:00.789\033[0m [\033[32mINFO \033[0m] \033[36mScheduler\033[0m - Running daily cleanup task +\033[90m2024-01-15 10:31:05.012\033[0m [\033[32mINFO \033[0m] \033[36mScheduler\033[0m - Cleanup completed: removed 1,234 expired sessions +\033[90m2024-01-15 10:31:05.123\033[0m [\033[32mINFO \033[0m] \033[36mScheduler\033[0m - Next cleanup scheduled for 2024-01-16 10:31:00` + + svgData, err := freeze.GenerateFromANSI(systemLogs) + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + err = os.WriteFile("output/system_logs.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Println("✅ Generated: output/system_logs.svg") +} diff --git a/examples/06-advanced/main.go b/examples/06-advanced/main.go new file mode 100644 index 0000000..450d3e7 --- /dev/null +++ b/examples/06-advanced/main.go @@ -0,0 +1,587 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("🔧 Advanced Configuration Examples") + fmt.Println("===================================") + + // Create output directory + os.MkdirAll("output", 0755) + + // Run advanced examples + customFontExample() + advancedLayoutExample() + performanceOptimizationExample() + responsiveDesignExample() + brandingExample() + + fmt.Println("\n✅ Advanced examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") +} + +// Custom font example +func customFontExample() { + fmt.Println("\n🔤 Custom Font Examples") + fmt.Println("-----------------------") + + code := `interface UserRepository { + findById(id: string): Promise; + findByEmail(email: string): Promise; + create(user: CreateUserDto): Promise; + update(id: string, updates: UpdateUserDto): Promise; + delete(id: string): Promise; +} + +class PostgresUserRepository implements UserRepository { + constructor(private db: Database) {} + + async findById(id: string): Promise { + const result = await this.db.query( + 'SELECT * FROM users WHERE id = $1', + [id] + ); + return result.rows[0] || null; + } + + async create(user: CreateUserDto): Promise { + const { name, email, role } = user; + const result = await this.db.query( + 'INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING *', + [name, email, role] + ); + return result.rows[0]; + } +}` + + // Different font configurations + fontConfigs := []struct { + name string + family string + size float64 + desc string + }{ + {"monospace_small", "Courier New", 12, "Small monospace font"}, + {"monospace_large", "JetBrains Mono", 16, "Large modern monospace"}, + {"system_font", "system-ui", 14, "System default font"}, + {"serif_font", "Georgia", 14, "Serif font for readability"}, + {"condensed_font", "SF Mono", 13, "Condensed font for more content"}, + } + + for _, config := range fontConfigs { + fmt.Printf("🔤 Generating %s example...\n", config.name) + + freeze := freezelib.New(). + WithTheme("github-dark"). + WithFont(config.family, config.size). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(25) + + svgData, err := freeze.GenerateFromCode(code, "typescript") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/font_%s.svg", config.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, config.desc) + } +} + +// Advanced layout example +func advancedLayoutExample() { + fmt.Println("\n📐 Advanced Layout Examples") + fmt.Println("---------------------------") + + code := `from dataclasses import dataclass +from typing import List, Optional, Dict, Any +from datetime import datetime +import asyncio +import aiohttp + +@dataclass +class APIResponse: + status_code: int + data: Dict[str, Any] + headers: Dict[str, str] + timestamp: datetime + +class AsyncAPIClient: + def __init__(self, base_url: str, timeout: int = 30): + self.base_url = base_url.rstrip('/') + self.timeout = aiohttp.ClientTimeout(total=timeout) + self.session: Optional[aiohttp.ClientSession] = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession(timeout=self.timeout) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def get(self, endpoint: str, params: Optional[Dict] = None) -> APIResponse: + if not self.session: + raise RuntimeError("Client not initialized. Use async context manager.") + + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + async with self.session.get(url, params=params) as response: + data = await response.json() + return APIResponse( + status_code=response.status, + data=data, + headers=dict(response.headers), + timestamp=datetime.now() + ) + +# Usage example +async def main(): + async with AsyncAPIClient("https://api.example.com") as client: + response = await client.get("/users", {"page": 1, "limit": 10}) + print(f"Status: {response.status_code}") + print(f"Data: {response.data}") + +if __name__ == "__main__": + asyncio.run(main())` + + // Different layout configurations + layouts := []struct { + name string + config func() *freezelib.Freeze + desc string + }{ + { + "compact", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("SF Mono", 11). + WithPadding(10). + WithMargin(5). + WithWindow(false). + WithLineNumbers(true). + WithDimensions(600, 800) + }, + "Compact layout for maximum content", + }, + { + "spacious", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github-dark"). + WithFont("JetBrains Mono", 16). + WithPadding(40). + WithMargin(30). + WithWindow(true). + WithLineNumbers(true). + WithShadow(25, 0, 15). + WithDimensions(1000, 1200) + }, + "Spacious layout for presentations", + }, + { + "mobile_friendly", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("dracula"). + WithFont("Menlo", 13). + WithPadding(15). + WithMargin(10). + WithWindow(false). + WithLineNumbers(false). + WithDimensions(400, 600) + }, + "Mobile-friendly narrow layout", + }, + { + "print_optimized", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("Times New Roman", 12). + WithPadding(20). + WithMargin(15). + WithWindow(false). + WithLineNumbers(true). + WithShadow(0, 0, 0). // No shadow for print + WithBackground("#ffffff"). + WithDimensions(800, 1000) + }, + "Print-optimized layout", + }, + } + + for _, layout := range layouts { + fmt.Printf("📐 Creating %s layout...\n", layout.name) + + freeze := layout.config() + svgData, err := freeze.GenerateFromCode(code, "python") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/layout_%s.svg", layout.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, layout.desc) + } +} + +// Performance optimization example +func performanceOptimizationExample() { + fmt.Println("\n⚡ Performance Optimization") + fmt.Println("---------------------------") + + // Short code for performance testing + shortCode := `fn quicksort(arr: &mut [T]) { + if arr.len() <= 1 { + return; + } + + let pivot_index = partition(arr); + let (left, right) = arr.split_at_mut(pivot_index); + + quicksort(left); + quicksort(&mut right[1..]); +} + +fn partition(arr: &mut [T]) -> usize { + let pivot_index = arr.len() - 1; + let mut i = 0; + + for j in 0..pivot_index { + if arr[j] <= arr[pivot_index] { + arr.swap(i, j); + i += 1; + } + } + + arr.swap(i, pivot_index); + i +}` + + // Performance-optimized configurations + perfConfigs := []struct { + name string + config func() *freezelib.Freeze + desc string + }{ + { + "minimal_overhead", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("monospace", 12). + WithWindow(false). + WithLineNumbers(false). + WithShadow(0, 0, 0). + WithPadding(10). + WithMargin(0) + }, + "Minimal processing overhead", + }, + { + "optimized_svg", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("system-ui", 13). + WithWindow(false). + WithLineNumbers(true). + WithShadow(0, 0, 0). + WithPadding(15). + WithDimensions(600, 400) // Fixed dimensions + }, + "SVG-optimized configuration", + }, + { + "batch_processing", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("monokai"). + WithFont("Courier", 12). + WithWindow(false). + WithLineNumbers(false). + WithPadding(12). + WithDimensions(500, 300) + }, + "Optimized for batch processing", + }, + } + + for _, config := range perfConfigs { + fmt.Printf("⚡ Testing %s...\n", config.name) + + freeze := config.config() + + // Generate multiple times to test performance + for i := 0; i < 3; i++ { + svgData, err := freeze.GenerateFromCode(shortCode, "rust") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + break + } + + filename := fmt.Sprintf("output/perf_%s_%d.svg", config.name, i+1) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + break + } + } + + fmt.Printf("✅ Generated 3 files for %s - %s\n", config.name, config.desc) + } +} + +// Responsive design example +func responsiveDesignExample() { + fmt.Println("\n📱 Responsive Design") + fmt.Println("--------------------") + + code := `@media (max-width: 768px) { + .container { + padding: 1rem; + margin: 0; + } + + .grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .card { + margin-bottom: 1rem; + } + + .navigation { + flex-direction: column; + } + + .nav-item { + width: 100%; + text-align: center; + padding: 0.75rem; + } +} + +@media (min-width: 769px) and (max-width: 1024px) { + .container { + max-width: 750px; + padding: 2rem; + } + + .grid { + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + } +} + +@media (min-width: 1025px) { + .container { + max-width: 1200px; + padding: 3rem; + } + + .grid { + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + } + + .hero { + height: 60vh; + display: flex; + align-items: center; + justify-content: center; + } +}` + + // Different screen size simulations + screenSizes := []struct { + name string + width float64 + height float64 + desc string + }{ + {"mobile", 375, 600, "Mobile phone size"}, + {"tablet", 768, 800, "Tablet size"}, + {"desktop", 1200, 800, "Desktop size"}, + {"ultrawide", 1600, 900, "Ultrawide monitor"}, + } + + for _, size := range screenSizes { + fmt.Printf("📱 Creating %s responsive example...\n", size.name) + + freeze := freezelib.New(). + WithTheme("github"). + WithFont("system-ui", 13). + WithDimensions(size.width, size.height). + WithWindow(true). + WithLineNumbers(true). + WithPadding(20). + WithShadow(10, 0, 5) + + svgData, err := freeze.GenerateFromCode(code, "css") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/responsive_%s.svg", size.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s (%dx%d) - %s\n", + filename, size.width, size.height, size.desc) + } +} + +// Branding example +func brandingExample() { + fmt.Println("\n🎨 Branding Examples") + fmt.Println("--------------------") + + code := `public class BrandService { + private final Logger logger = LoggerFactory.getLogger(BrandService.class); + private final BrandRepository brandRepository; + private final CacheManager cacheManager; + + public BrandService(BrandRepository brandRepository, CacheManager cacheManager) { + this.brandRepository = brandRepository; + this.cacheManager = cacheManager; + } + + @Cacheable("brands") + public Brand getBrandById(Long id) { + logger.info("Fetching brand with id: {}", id); + + return brandRepository.findById(id) + .orElseThrow(() -> new BrandNotFoundException("Brand not found: " + id)); + } + + @Transactional + public Brand createBrand(CreateBrandRequest request) { + validateBrandRequest(request); + + Brand brand = Brand.builder() + .name(request.getName()) + .description(request.getDescription()) + .logoUrl(request.getLogoUrl()) + .primaryColor(request.getPrimaryColor()) + .secondaryColor(request.getSecondaryColor()) + .createdAt(Instant.now()) + .build(); + + Brand savedBrand = brandRepository.save(brand); + cacheManager.evictCache("brands"); + + logger.info("Created new brand: {}", savedBrand.getName()); + return savedBrand; + } + + private void validateBrandRequest(CreateBrandRequest request) { + if (StringUtils.isBlank(request.getName())) { + throw new ValidationException("Brand name is required"); + } + + if (brandRepository.existsByName(request.getName())) { + throw new ValidationException("Brand name already exists"); + } + } +}` + + // Different brand styles + brandStyles := []struct { + name string + config func() *freezelib.Freeze + desc string + }{ + { + "corporate_blue", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github"). + WithFont("Arial", 14). + WithBackground("#f8f9fa"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(8, 2, 4). + WithBorder(2, 8, "#0066cc"). + WithPadding(30) + }, + "Corporate blue branding", + }, + { + "startup_green", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("github-dark"). + WithFont("Inter", 14). + WithBackground("#0d1117"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 10). + WithBorder(1, 12, "#00d084"). + WithPadding(25) + }, + "Startup green branding", + }, + { + "creative_purple", + func() *freezelib.Freeze { + return freezelib.New(). + WithTheme("dracula"). + WithFont("Poppins", 14). + WithBackground("#1a1a2e"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(20, 0, 15). + WithBorder(2, 16, "#8b5cf6"). + WithPadding(35) + }, + "Creative purple branding", + }, + } + + for _, style := range brandStyles { + fmt.Printf("🎨 Creating %s style...\n", style.name) + + freeze := style.config() + svgData, err := freeze.GenerateFromCode(code, "java") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + filename := fmt.Sprintf("output/brand_%s.svg", style.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + fmt.Printf("✅ Generated: %s - %s\n", filename, style.desc) + } +} diff --git a/examples/07-batch/main.go b/examples/07-batch/main.go new file mode 100644 index 0000000..c4440d2 --- /dev/null +++ b/examples/07-batch/main.go @@ -0,0 +1,680 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("📦 Batch Processing Examples") + fmt.Println("=============================") + + // Create output directory + os.MkdirAll("output", 0755) + os.MkdirAll("sample_files", 0755) + + // Run batch examples + createSampleFiles() + batchFileProcessingExample() + multiFormatBatchExample() + concurrentProcessingExample() + directoryProcessingExample() + + fmt.Println("\n✅ Batch processing examples completed!") + fmt.Println("📁 Check the 'output' directory for generated files.") +} + +// Create sample files for batch processing +func createSampleFiles() { + fmt.Println("\n📝 Creating Sample Files") + fmt.Println("------------------------") + + sampleFiles := map[string]string{ + "sample_files/main.go": `package main + +import ( + "fmt" + "net/http" + "log" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") + }) + + fmt.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +}`, + + "sample_files/utils.py": `import json +import logging +from typing import Dict, List, Any, Optional +from datetime import datetime + +logger = logging.getLogger(__name__) + +def load_config(config_path: str) -> Dict[str, Any]: + """Load configuration from JSON file.""" + try: + with open(config_path, 'r') as f: + return json.load(f) + except FileNotFoundError: + logger.error(f"Config file not found: {config_path}") + return {} + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in config file: {e}") + return {} + +def format_timestamp(timestamp: Optional[datetime] = None) -> str: + """Format timestamp to ISO string.""" + if timestamp is None: + timestamp = datetime.now() + return timestamp.isoformat() + +class DataProcessor: + def __init__(self, config: Dict[str, Any]): + self.config = config + self.processed_count = 0 + + def process_batch(self, items: List[Any]) -> List[Any]: + """Process a batch of items.""" + results = [] + for item in items: + processed = self.process_item(item) + if processed: + results.append(processed) + self.processed_count += 1 + return results + + def process_item(self, item: Any) -> Optional[Any]: + """Process a single item.""" + # Implementation depends on item type + return item`, + + "sample_files/api.js": `const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP' +}); +app.use('/api/', limiter); + +// Routes +app.get('/api/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); +}); + +app.get('/api/users', async (req, res) => { + try { + const { page = 1, limit = 10 } = req.query; + const users = await getUsersPaginated(page, limit); + + res.json({ + data: users, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: await getTotalUsers() + } + }); + } catch (error) { + console.error('Error fetching users:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.listen(PORT, () => { + console.log('Server running on port \${PORT}\'); +});`, + + "sample_files/styles.css": `/* Modern CSS Reset and Base Styles */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --primary-color: #3b82f6; + --secondary-color: #64748b; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; + --background-color: #ffffff; + --surface-color: #f8fafc; + --text-primary: #1e293b; + --text-secondary: #64748b; + --border-color: #e2e8f0; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--background-color); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.card { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + box-shadow: var(--shadow); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 6px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: #2563eb; + transform: translateY(-1px); +}`, + + "sample_files/config.json": `{ + "database": { + "host": "localhost", + "port": 5432, + "name": "myapp", + "user": "postgres", + "password": "password", + "ssl": false, + "pool": { + "min": 2, + "max": 10, + "idle_timeout": "30s" + } + }, + "redis": { + "host": "localhost", + "port": 6379, + "password": "", + "db": 0, + "pool_size": 10 + }, + "server": { + "host": "0.0.0.0", + "port": 8080, + "read_timeout": "30s", + "write_timeout": "30s", + "idle_timeout": "60s" + }, + "logging": { + "level": "info", + "format": "json", + "output": "stdout" + }, + "features": { + "enable_metrics": true, + "enable_tracing": true, + "enable_profiling": false + } +}`, + } + + for filename, content := range sampleFiles { + err := os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("❌ Error creating %s: %v\n", filename, err) + continue + } + fmt.Printf("✅ Created: %s\n", filename) + } +} + +// Batch file processing example +func batchFileProcessingExample() { + fmt.Println("\n📦 Batch File Processing") + fmt.Println("------------------------") + + // Get all sample files + files, err := filepath.Glob("sample_files/*") + if err != nil { + fmt.Printf("❌ Error finding files: %v\n", err) + return + } + + // Create a consistent freeze instance for all files + freeze := freezelib.New(). + WithTheme("github-dark"). + WithFont("JetBrains Mono", 14). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(20) + + fmt.Printf("🔄 Processing %d files...\n", len(files)) + + successCount := 0 + for _, file := range files { + fmt.Printf("📄 Processing: %s\n", file) + + // Detect language from file extension + ext := filepath.Ext(file) + lang := detectLanguage(ext) + + svgData, err := freeze.GenerateFromFile(file) + if err != nil { + fmt.Printf("❌ Error processing %s: %v\n", file, err) + continue + } + + // Create output filename + baseName := strings.TrimSuffix(filepath.Base(file), ext) + outputFile := fmt.Sprintf("output/batch_%s.svg", baseName) + + err = os.WriteFile(outputFile, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving %s: %v\n", outputFile, err) + continue + } + + fmt.Printf("✅ Generated: %s (language: %s)\n", outputFile, lang) + successCount++ + } + + fmt.Printf("📊 Batch processing completed: %d/%d files successful\n", + successCount, len(files)) +} + +// Multi-format batch example +func multiFormatBatchExample() { + fmt.Println("\n🎨 Multi-format Batch Processing") + fmt.Println("--------------------------------") + + code := `#include +#include +#include +#include + +template +class SmartVector { +private: + std::unique_ptr data; + size_t size_; + size_t capacity_; + +public: + SmartVector(size_t initial_capacity = 10) + : data(std::make_unique(initial_capacity)) + , size_(0) + , capacity_(initial_capacity) {} + + void push_back(const T& value) { + if (size_ >= capacity_) { + resize(); + } + data[size_++] = value; + } + + T& operator[](size_t index) { + if (index >= size_) { + throw std::out_of_range("Index out of range"); + } + return data[index]; + } + + size_t size() const { return size_; } + + void sort() { + std::sort(data.get(), data.get() + size_); + } + +private: + void resize() { + capacity_ *= 2; + auto new_data = std::make_unique(capacity_); + std::copy(data.get(), data.get() + size_, new_data.get()); + data = std::move(new_data); + } +}; + +int main() { + SmartVector vec; + + for (int i = 0; i < 15; ++i) { + vec.push_back(rand() % 100); + } + + vec.sort(); + + std::cout << "Sorted vector: "; + for (size_t i = 0; i < vec.size(); ++i) { + std::cout << vec[i] << " "; + } + std::cout << std::endl; + + return 0; +}` + + // Different format configurations + formats := []struct { + name string + format string + theme string + }{ + {"svg_light", "svg", "github"}, + {"svg_dark", "svg", "github-dark"}, + {"png_presentation", "png", "dracula"}, + {"png_print", "png", "github"}, + } + + freeze := freezelib.New(). + WithFont("Cascadia Code", 14). + WithWindow(true). + WithLineNumbers(true). + WithShadow(15, 0, 8). + WithPadding(25) + + for _, format := range formats { + fmt.Printf("🎨 Generating %s format...\n", format.name) + + freeze.WithTheme(format.theme) + + var data []byte + var err error + var filename string + + if format.format == "svg" { + data, err = freeze.GenerateFromCode(code, "cpp") + filename = fmt.Sprintf("output/multiformat_%s.svg", format.name) + } else { + data, err = freeze.GeneratePNGFromCode(code, "cpp") + filename = fmt.Sprintf("output/multiformat_%s.png", format.name) + } + + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + continue + } + + err = os.WriteFile(filename, data, 0644) + if err != nil { + fmt.Printf("❌ Error saving: %v\n", err) + continue + } + + // Show file size + info, _ := os.Stat(filename) + fmt.Printf("✅ Generated: %s (%d bytes)\n", filename, info.Size()) + } +} + +// Concurrent processing example +func concurrentProcessingExample() { + fmt.Println("\n⚡ Concurrent Processing") + fmt.Println("-----------------------") + + // Sample code snippets for concurrent processing + codeSnippets := []struct { + name string + code string + lang string + }{ + { + "snippet1", + `def fibonacci(n): + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +print([fibonacci(i) for i in range(10)])`, + "python", + }, + { + "snippet2", + `function quickSort(arr) { + if (arr.length <= 1) return arr; + + const pivot = arr[Math.floor(arr.length / 2)]; + const left = arr.filter(x => x < pivot); + const middle = arr.filter(x => x === pivot); + const right = arr.filter(x => x > pivot); + + return [...quickSort(left), ...middle, ...quickSort(right)]; +}`, + "javascript", + }, + { + "snippet3", + `public class BinarySearch { + public static int search(int[] arr, int target) { + int left = 0, right = arr.length - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + + if (arr[mid] == target) return mid; + if (arr[mid] < target) left = mid + 1; + else right = mid - 1; + } + + return -1; + } +}`, + "java", + }, + { + "snippet4", + `use std::collections::HashMap; + +fn word_count(text: &str) -> HashMap { + text.split_whitespace() + .map(|word| word.to_lowercase()) + .fold(HashMap::new(), |mut acc, word| { + *acc.entry(word).or_insert(0) += 1; + acc + }) +}`, + "rust", + }, + } + + // Create freeze instance + freeze := freezelib.New(). + WithTheme("nord"). + WithFont("JetBrains Mono", 13). + WithWindow(true). + WithLineNumbers(true). + WithShadow(10, 0, 5). + WithPadding(20) + + // Use goroutines for concurrent processing + var wg sync.WaitGroup + results := make(chan string, len(codeSnippets)) + + fmt.Printf("🚀 Processing %d snippets concurrently...\n", len(codeSnippets)) + + for _, snippet := range codeSnippets { + wg.Add(1) + go func(s struct { + name string + code string + lang string + }) { + defer wg.Done() + + svgData, err := freeze.GenerateFromCode(s.code, s.lang) + if err != nil { + results <- fmt.Sprintf("❌ Error processing %s: %v", s.name, err) + return + } + + filename := fmt.Sprintf("output/concurrent_%s.svg", s.name) + err = os.WriteFile(filename, svgData, 0644) + if err != nil { + results <- fmt.Sprintf("❌ Error saving %s: %v", filename, err) + return + } + + results <- fmt.Sprintf("✅ Generated: %s", filename) + }(snippet) + } + + // Wait for all goroutines to complete + go func() { + wg.Wait() + close(results) + }() + + // Collect results + for result := range results { + fmt.Println(result) + } +} + +// Directory processing example +func directoryProcessingExample() { + fmt.Println("\n📁 Directory Processing") + fmt.Println("-----------------------") + + // Process all files in sample_files directory + err := filepath.Walk("sample_files", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + // Only process certain file types + ext := filepath.Ext(path) + if !isSupportedFile(ext) { + return nil + } + + fmt.Printf("📄 Processing directory file: %s\n", path) + + // Create themed freeze instance based on file type + theme := getThemeForFile(ext) + freeze := freezelib.New(). + WithTheme(theme). + WithFont("SF Mono", 13). + WithWindow(true). + WithLineNumbers(true). + WithPadding(20) + + svgData, err := freeze.GenerateFromFile(path) + if err != nil { + fmt.Printf("❌ Error processing %s: %v\n", path, err) + return nil + } + + // Create output filename + baseName := strings.TrimSuffix(filepath.Base(path), ext) + outputFile := fmt.Sprintf("output/directory_%s.svg", baseName) + + err = os.WriteFile(outputFile, svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving %s: %v\n", outputFile, err) + return nil + } + + fmt.Printf("✅ Generated: %s (theme: %s)\n", outputFile, theme) + return nil + }) + + if err != nil { + fmt.Printf("❌ Error walking directory: %v\n", err) + } +} + +// Helper functions +func detectLanguage(ext string) string { + switch ext { + case ".go": + return "go" + case ".py": + return "python" + case ".js": + return "javascript" + case ".css": + return "css" + case ".json": + return "json" + default: + return "text" + } +} + +func isSupportedFile(ext string) bool { + supported := []string{".go", ".py", ".js", ".css", ".json", ".md", ".txt"} + for _, s := range supported { + if ext == s { + return true + } + } + return false +} + +func getThemeForFile(ext string) string { + switch ext { + case ".go": + return "github-dark" + case ".py": + return "monokai" + case ".js": + return "dracula" + case ".css": + return "github" + case ".json": + return "nord" + default: + return "github" + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..580268a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,113 @@ +# FreezeLib Examples + +**Language / 语言**: [English](README.md) | [中文](README_CN.md) + +This directory contains comprehensive examples demonstrating various features of FreezeLib. + +## 📁 Example Categories + +### [01-basic/](01-basic/) - Basic Usage +- Simple code screenshot generation +- Basic configuration +- Getting started examples + +### [02-formats/](02-formats/) - Output Formats +- SVG output examples +- PNG output examples +- Format comparison +- Quality settings + +### [03-themes/](03-themes/) - Theme Showcase +- Popular themes demonstration +- Theme comparison +- Custom theme creation + +### [04-languages/](04-languages/) - Programming Languages +- Syntax highlighting for different languages +- Language-specific optimizations +- Multi-language projects + +### [05-terminal/](05-terminal/) - Terminal Output +- ANSI color support +- Terminal styling +- Command output screenshots + +### [06-advanced/](06-advanced/) - Advanced Configuration +- Complex styling options +- Performance optimization +- Custom fonts and layouts + +### [07-batch/](07-batch/) - Batch Processing +- Multiple file processing +- Automated workflows +- Bulk operations + +## 🚀 Quick Start + +To run all examples: + +```bash +cd examples +go run run_all_examples.go +``` + +To run specific category: + +```bash +cd examples/01-basic +go run main.go +``` + +## 📊 Output Formats + +Each example category demonstrates: +- **SVG**: Vector format, perfect for web and documentation +- **PNG**: Raster format, ideal for presentations and social media +- **Quality comparisons**: Different settings and their effects + +## 🎨 Visual Features Demonstrated + +- **Syntax Highlighting**: 100+ programming languages +- **Themes**: Light, dark, and custom themes +- **Window Decorations**: macOS-style window controls +- **Line Numbers**: Optional line numbering +- **Shadows and Borders**: Visual enhancement effects +- **Custom Fonts**: Typography options +- **ANSI Colors**: Terminal output rendering +- **Responsive Sizing**: Adaptive dimensions + +## 📝 Code Examples Include + +- **Web Development**: HTML, CSS, JavaScript, TypeScript +- **Backend**: Go, Python, Java, C#, Rust +- **Mobile**: Swift, Kotlin, Dart +- **DevOps**: Docker, YAML, Shell scripts +- **Data**: SQL, JSON, CSV processing +- **Documentation**: Markdown, configuration files + +## 🔧 Configuration Examples + +Each category includes examples of: +- Basic configuration +- Advanced customization +- Performance optimization +- Error handling +- Best practices + +## 📖 Learning Path + +1. **Start with [01-basic/](01-basic/)** - Learn fundamental concepts +2. **Explore [02-formats/](02-formats/)** - Understand output options +3. **Try [03-themes/](03-themes/)** - Discover visual styles +4. **Check [04-languages/](04-languages/)** - See language support +5. **Advanced topics** - Dive into specialized use cases + +## 🤝 Contributing Examples + +To add new examples: + +1. Choose appropriate category or create new one +2. Follow the naming convention: `example_name.go` +3. Include both code and generated output +4. Add documentation in README.md +5. Test with `go run main.go` \ No newline at end of file diff --git a/examples/README_CN.md b/examples/README_CN.md new file mode 100644 index 0000000..6cc1cf8 --- /dev/null +++ b/examples/README_CN.md @@ -0,0 +1,114 @@ +# FreezeLib 示例集合 + +**Language / 语言**: [English](README.md) | [中文](README_CN.md) + +本目录包含了展示 FreezeLib 各种功能的综合示例。 + +## 📁 示例分类 + +### [01-basic/](01-basic/) - 基础用法 +- 简单代码截图生成 +- 基本配置 +- 入门示例 + +### [02-formats/](02-formats/) - 输出格式 +- SVG 输出示例 +- PNG 输出示例 +- 格式对比 +- 质量设置 + +### [03-themes/](03-themes/) - 主题展示 +- 流行主题演示 +- 主题对比 +- 自定义主题创建 + +### [04-languages/](04-languages/) - 编程语言 +- 不同语言的语法高亮 +- 语言特定优化 +- 多语言项目 + +### [05-terminal/](05-terminal/) - 终端输出 +- ANSI 颜色支持 +- 终端样式 +- 命令输出截图 + +### [06-advanced/](06-advanced/) - 高级配置 +- 复杂样式选项 +- 性能优化 +- 自定义字体和布局 + +### [07-batch/](07-batch/) - 批量处理 +- 多文件处理 +- 自动化工作流 +- 批量操作 + +## 🚀 快速开始 + +运行所有示例: + +```bash +cd examples +go run run_all_examples.go +``` + +运行特定分类: + +```bash +cd examples/01-basic +go run main.go +``` + +## 📊 输出格式 + +每个示例分类都演示: +- **SVG**: 矢量格式,完美适用于网页和文档 +- **PNG**: 栅格格式,适合演示和社交媒体 +- **质量对比**: 不同设置及其效果 + +## 🎨 展示的视觉功能 + +- **语法高亮**: 100+ 种编程语言 +- **主题**: 浅色、深色和自定义主题 +- **窗口装饰**: macOS 风格窗口控件 +- **行号**: 可选行号显示 +- **阴影和边框**: 视觉增强效果 +- **自定义字体**: 排版选项 +- **ANSI 颜色**: 终端输出渲染 +- **响应式尺寸**: 自适应尺寸 + +## 📝 代码示例包括 + +- **Web 开发**: HTML, CSS, JavaScript, TypeScript +- **后端**: Go, Python, Java, C#, Rust +- **移动端**: Swift, Kotlin, Dart +- **DevOps**: Docker, YAML, Shell 脚本 +- **数据**: SQL, JSON, CSV 处理 +- **文档**: Markdown, 配置文件 + +## 🔧 配置示例 + +每个分类都包含以下示例: +- 基本配置 +- 高级自定义 +- 性能优化 +- 错误处理 +- 最佳实践 + +## 📖 学习路径 + +1. **从 [01-basic/](01-basic/) 开始** - 学习基本概念 +2. **探索 [02-formats/](02-formats/)** - 了解输出选项 +3. **尝试 [03-themes/](03-themes/)** - 发现视觉样式 +4. **查看 [04-languages/](04-languages/)** - 了解语言支持 +5. **高级主题** - 深入专业用例 + +## 🤝 贡献示例 + +添加新示例: + +1. 选择合适的分类或创建新分类 +2. 遵循命名约定:`example_name.go` +3. 包含代码和生成的输出 +4. 在 README.md 中添加文档 +5. 使用 `go run main.go` 测试 + diff --git a/font/JetBrainsMono-Regular.ttf b/font/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..3b75b26 Binary files /dev/null and b/font/JetBrainsMono-Regular.ttf differ diff --git a/font/JetBrainsMonoNL-Regular.ttf b/font/JetBrainsMonoNL-Regular.ttf new file mode 100644 index 0000000..70d2ec9 Binary files /dev/null and b/font/JetBrainsMonoNL-Regular.ttf differ diff --git a/font/font.go b/font/font.go new file mode 100644 index 0000000..4425bb8 --- /dev/null +++ b/font/font.go @@ -0,0 +1,165 @@ +package font + +import ( + "embed" + "fmt" + + formatter "github.com/alecthomas/chroma/v2/formatters/svg" +) + +//go:embed *.ttf +var fonts embed.FS + +// JetBrainsMonoTTF contains the JetBrains Mono font data +var JetBrainsMonoTTF []byte + +// JetBrainsMonoNLTTF contains the JetBrains Mono NL font data +var JetBrainsMonoNLTTF []byte + +func init() { + var err error + JetBrainsMonoTTF, err = fonts.ReadFile("JetBrainsMono-Regular.ttf") + if err != nil { + // If embedded font is not available, use empty slice + JetBrainsMonoTTF = []byte{} + } + + JetBrainsMonoNLTTF, err = fonts.ReadFile("JetBrainsMonoNL-Regular.ttf") + if err != nil { + // If embedded font is not available, use empty slice + JetBrainsMonoNLTTF = []byte{} + } +} + +// FontOptions creates formatter options for the given font configuration +func FontOptions(family string, size float64, ligatures bool, fontFile string) ([]formatter.Option, error) { + var options []formatter.Option + + // Set font family + if family != "" { + options = append(options, formatter.FontFamily(family)) + } + + // Embed font file if specified + if fontFile != "" { + option, err := formatter.EmbedFontFile(family, fontFile) + if err != nil { + return nil, fmt.Errorf("failed to embed font file: %w", err) + } + options = append(options, option) + } + + return options, nil +} + +// GetDefaultFontFamily returns the default font family +func GetDefaultFontFamily() string { + return "JetBrains Mono" +} + +// GetDefaultFontSize returns the default font size +func GetDefaultFontSize() float64 { + return 14.0 +} + +// IsMonospaceFont checks if a font family is monospace +func IsMonospaceFont(family string) bool { + monospaceFonts := map[string]bool{ + "JetBrains Mono": true, + "Fira Code": true, + "Source Code Pro": true, + "Monaco": true, + "Menlo": true, + "Consolas": true, + "Courier New": true, + "monospace": true, + "SF Mono": true, + "Cascadia Code": true, + "Ubuntu Mono": true, + "DejaVu Sans Mono": true, + "Liberation Mono": true, + "Inconsolata": true, + "Roboto Mono": true, + } + return monospaceFonts[family] +} + +// ValidateFontFamily validates if a font family name is valid +func ValidateFontFamily(family string) error { + if family == "" { + return fmt.Errorf("font family cannot be empty") + } + return nil +} + +// ValidateFontSize validates if a font size is valid +func ValidateFontSize(size float64) error { + if size <= 0 { + return fmt.Errorf("font size must be positive, got %.2f", size) + } + if size > 100 { + return fmt.Errorf("font size too large, got %.2f", size) + } + return nil +} + +// GetFontHeightToWidthRatio returns the typical height to width ratio for monospace fonts +func GetFontHeightToWidthRatio() float64 { + return 1.68 +} + +// CalculateTextWidth estimates the width of text in pixels +func CalculateTextWidth(text string, fontSize float64) float64 { + return float64(len(text)) * (fontSize / GetFontHeightToWidthRatio()) +} + +// CalculateLineHeight calculates the line height in pixels +func CalculateLineHeight(fontSize, lineHeightRatio float64) float64 { + return fontSize * lineHeightRatio +} + +// GetEmbeddedFontData returns embedded font data if available +func GetEmbeddedFontData(fontName string) []byte { + switch fontName { + case "JetBrains Mono", "JetBrainsMono": + return JetBrainsMonoTTF + case "JetBrains Mono NL", "JetBrainsMonoNL": + return JetBrainsMonoNLTTF + default: + return nil + } +} + +// FontConfig represents font configuration +type FontConfig struct { + Family string + Size float64 + Ligatures bool + File string +} + +// NewFontConfig creates a new font configuration with defaults +func NewFontConfig() *FontConfig { + return &FontConfig{ + Family: GetDefaultFontFamily(), + Size: GetDefaultFontSize(), + Ligatures: true, + File: "", + } +} + +// Validate validates the font configuration +func (fc *FontConfig) Validate() error { + if err := ValidateFontFamily(fc.Family); err != nil { + return err + } + if err := ValidateFontSize(fc.Size); err != nil { + return err + } + return nil +} + +// ToFormatterOptions converts font config to formatter options +func (fc *FontConfig) ToFormatterOptions() ([]formatter.Option, error) { + return FontOptions(fc.Family, fc.Size, fc.Ligatures, fc.File) +} diff --git a/freeze.go b/freeze.go new file mode 100644 index 0000000..aeb9dd2 --- /dev/null +++ b/freeze.go @@ -0,0 +1,310 @@ +// Package freezelib provides a Go library for generating beautiful code screenshots +// from source code and terminal output. +// +// This library is based on the freeze CLI tool by Charm and provides a programmatic +// interface for creating code screenshots with syntax highlighting, themes, and +// various styling options. +package freezelib + +import ( + "fmt" + "io" + "os" +) + +// Freeze is the main interface for generating code screenshots +type Freeze struct { + generator *Generator + config *Config +} + +// New creates a new Freeze instance with default configuration +func New() *Freeze { + config := DefaultConfig() + return &Freeze{ + generator: NewGenerator(config), + config: config, + } +} + +// NewWithConfig creates a new Freeze instance with the provided configuration +func NewWithConfig(config *Config) *Freeze { + if config == nil { + config = DefaultConfig() + } + return &Freeze{ + generator: NewGenerator(config), + config: config, + } +} + +// NewWithPreset creates a new Freeze instance with a preset configuration +func NewWithPreset(presetName string) *Freeze { + config := GetPreset(presetName) + return &Freeze{ + generator: NewGenerator(config), + config: config, + } +} + +// Config returns the current configuration +func (f *Freeze) Config() *Config { + return f.config +} + +// SetConfig updates the configuration and recreates the generator +func (f *Freeze) SetConfig(config *Config) *Freeze { + f.config = config + f.generator = NewGenerator(config) + return f +} + +// UpdateConfig allows modifying the current configuration +func (f *Freeze) UpdateConfig(fn func(*Config)) *Freeze { + fn(f.config) + f.generator = NewGenerator(f.config) + return f +} + +// GenerateFromCode generates an SVG screenshot from source code +func (f *Freeze) GenerateFromCode(code, language string) ([]byte, error) { + return f.generator.GenerateFromCode(code, language) +} + +// GenerateFromFile generates an SVG screenshot from a source code file +func (f *Freeze) GenerateFromFile(filename string) ([]byte, error) { + return f.generator.GenerateFromFile(filename) +} + +// GenerateFromReader generates an SVG screenshot from a reader containing source code +func (f *Freeze) GenerateFromReader(reader io.Reader, language string) ([]byte, error) { + content, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("failed to read from reader: %w", err) + } + return f.generator.GenerateFromCode(string(content), language) +} + +// GenerateFromANSI generates an SVG screenshot from ANSI terminal output +func (f *Freeze) GenerateFromANSI(ansiOutput string) ([]byte, error) { + return f.generator.GenerateFromANSI(ansiOutput) +} + +// GeneratePNGFromCode generates a PNG screenshot from source code +func (f *Freeze) GeneratePNGFromCode(code, language string) ([]byte, error) { + svgData, err := f.generator.GenerateFromCode(code, language) + 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) + if err != nil { + return nil, err + } + + // Calculate dimensions for PNG + width := f.config.Width + height := f.config.Height + if width == 0 || height == 0 { + width = 800 * 4 + height = 600 * 4 + } else { + width *= 4 + height *= 4 + } + + return f.generator.ConvertToPNG(svgData, width, height) +} + +// GeneratePNGFromANSI generates a PNG screenshot from ANSI terminal output +func (f *Freeze) GeneratePNGFromANSI(ansiOutput string) ([]byte, error) { + svgData, err := f.generator.GenerateFromANSI(ansiOutput) + if err != nil { + return nil, err + } + + // Calculate dimensions for PNG + width := f.config.Width + height := f.config.Height + if width == 0 || height == 0 { + width = 800 * 4 + height = 600 * 4 + } else { + width *= 4 + height *= 4 + } + + return f.generator.ConvertToPNG(svgData, width, height) +} + +// SaveToFile saves the generated SVG to a file +func (f *Freeze) SaveToFile(data []byte, filename string) error { + return os.WriteFile(filename, data, 0644) +} + +// SaveCodeToFile generates and saves a code screenshot to a file +func (f *Freeze) SaveCodeToFile(code, language, filename string) error { + var data []byte + var err error + + if isPNGFile(filename) { + data, err = f.GeneratePNGFromCode(code, language) + } else { + data, err = f.GenerateFromCode(code, language) + } + + 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 + var err error + + if isPNGFile(outputFile) { + data, err = f.GeneratePNGFromFile(inputFile) + } else { + data, err = f.GenerateFromFile(inputFile) + } + + if err != nil { + return err + } + + return f.SaveToFile(data, outputFile) +} + +// SaveANSIToFile generates and saves an ANSI screenshot to a file +func (f *Freeze) SaveANSIToFile(ansiOutput, filename string) error { + var data []byte + var err error + + if isPNGFile(filename) { + data, err = f.GeneratePNGFromANSI(ansiOutput) + } else { + data, err = f.GenerateFromANSI(ansiOutput) + } + + if err != nil { + return err + } + + return f.SaveToFile(data, filename) +} + +// Clone creates a copy of the Freeze instance with the same configuration +func (f *Freeze) Clone() *Freeze { + return NewWithConfig(f.config.Clone()) +} + +// WithTheme creates a new Freeze instance with the specified theme +func (f *Freeze) WithTheme(theme string) *Freeze { + clone := f.Clone() + clone.config.SetTheme(theme) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithFont creates a new Freeze instance with the specified font +func (f *Freeze) WithFont(family string, size float64) *Freeze { + clone := f.Clone() + clone.config.SetFont(family, size) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithBackground creates a new Freeze instance with the specified background color +func (f *Freeze) WithBackground(color string) *Freeze { + clone := f.Clone() + clone.config.SetBackground(color) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithWindow creates a new Freeze instance with window controls enabled/disabled +func (f *Freeze) WithWindow(enabled bool) *Freeze { + clone := f.Clone() + clone.config.SetWindow(enabled) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithLineNumbers creates a new Freeze instance with line numbers enabled/disabled +func (f *Freeze) WithLineNumbers(enabled bool) *Freeze { + clone := f.Clone() + clone.config.SetLineNumbers(enabled) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithShadow creates a new Freeze instance with shadow settings +func (f *Freeze) WithShadow(blur, x, y float64) *Freeze { + clone := f.Clone() + clone.config.SetShadow(blur, x, y) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithBorder creates a new Freeze instance with border settings +func (f *Freeze) WithBorder(width, radius float64, color string) *Freeze { + clone := f.Clone() + clone.config.SetBorder(width, radius, color) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithPadding creates a new Freeze instance with padding settings +func (f *Freeze) WithPadding(values ...float64) *Freeze { + clone := f.Clone() + clone.config.SetPadding(values...) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithMargin creates a new Freeze instance with margin settings +func (f *Freeze) WithMargin(values ...float64) *Freeze { + clone := f.Clone() + clone.config.SetMargin(values...) + clone.generator = NewGenerator(clone.config) + return clone +} + +// WithDimensions creates a new Freeze instance with specific dimensions +func (f *Freeze) WithDimensions(width, height float64) *Freeze { + clone := f.Clone() + clone.config.SetDimensions(width, height) + clone.generator = NewGenerator(clone.config) + return clone +} + +// isPNGFile checks if the filename has a PNG extension +func isPNGFile(filename string) bool { + return len(filename) > 4 && filename[len(filename)-4:] == ".png" +} + +// Version information +const ( + Version = "1.0.0" + Author = "Charm" +) diff --git a/generator.go b/generator.go new file mode 100644 index 0000000..b142ea6 --- /dev/null +++ b/generator.go @@ -0,0 +1,443 @@ +package freezelib + +import ( + "bytes" + "context" + "errors" + "fmt" + "github.com/landaiqing/freezelib/font" + "github.com/landaiqing/freezelib/svg" + "os" + "strings" + + "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" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/cellbuf" + "github.com/kanrichan/resvg-go" +) + +const ( + defaultFontSize = 14.0 + defaultLineHeight = 1.2 +) + +// Generator handles the core screenshot generation logic +type Generator struct { + config *Config +} + +// NewGenerator creates a new generator with the given configuration +func NewGenerator(config *Config) *Generator { + if config == nil { + config = DefaultConfig() + } + return &Generator{config: config} +} + +// GenerateFromCode generates an SVG from source code +func (g *Generator) GenerateFromCode(code, language string) ([]byte, error) { + if err := g.config.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // Set language if provided + if language != "" { + 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) + } + if lexer == nil { + return nil, errors.New("could not determine language for syntax highlighting") + } + + return g.generateSVG(code, lexer, false) +} + +// GenerateFromFile generates an SVG from a source code file +func (g *Generator) GenerateFromFile(filename string) ([]byte, error) { + if err := g.config.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // Read file content + content, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + code := string(content) + + // Get lexer from filename + lexer := lexers.Get(filename) + if lexer == nil { + lexer = lexers.Analyse(code) + } + if lexer == nil { + return nil, errors.New("could not determine language for syntax highlighting") + } + + return g.generateSVG(code, lexer, false) +} + +// GenerateFromANSI generates an SVG from ANSI terminal output +func (g *Generator) GenerateFromANSI(ansiOutput string) ([]byte, error) { + if err := g.config.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // For ANSI output, we use a text lexer but handle ANSI sequences specially + strippedInput := ansi.Strip(ansiOutput) + it := chroma.Literator(chroma.Token{Type: chroma.Text, Value: strippedInput}) + + return g.generateSVGFromIterator(ansiOutput, it, true) +} + +// generateSVG is the core SVG generation function +func (g *Generator) generateSVG(input string, lexer chroma.Lexer, isAnsi bool) ([]byte, error) { + // Create token iterator + var it chroma.Iterator + var err error + if isAnsi { + strippedInput := ansi.Strip(input) + it = chroma.Literator(chroma.Token{Type: chroma.Text, Value: strippedInput}) + } else { + it, err = chroma.Coalesce(lexer).Tokenise(nil, input) + if err != nil { + return nil, fmt.Errorf("could not tokenize input: %w", err) + } + } + + return g.generateSVGFromIterator(input, it, isAnsi) +} + +// generateSVGFromIterator generates SVG from a token iterator +func (g *Generator) generateSVGFromIterator(input string, it chroma.Iterator, isAnsi bool) ([]byte, error) { + config := g.config + + // Calculate scale factor + scale := 1.0 + autoHeight := config.Height == 0 + autoWidth := config.Width == 0 + + // Expand padding and margin + expandedMargin := config.expandMargin(scale) + expandedPadding := config.expandPadding(scale) + + // Process input based on line selection + processedInput := input + if len(config.Lines) == 2 { + processedInput = cutLines(input, config.Lines) + } + + // Handle text wrapping + if config.Wrap > 0 { + processedInput = cellbuf.Wrap(processedInput, config.Wrap, "") + } + + // Get style + style, ok := styles.Registry[strings.ToLower(config.Theme)] + if !ok || style == nil { + style = styles.Get("github") // fallback to github style + } + + // Add background color to style if not present + if !style.Has(chroma.Background) { + var err error + style, err = style.Builder().Add(chroma.Background, "bg:"+config.Background).Build() + if err != nil { + return nil, fmt.Errorf("could not add background: %w", err) + } + } + + // Get font options + fontOptions, err := font.FontOptions(config.Font.Family, config.Font.Size, config.Font.Ligatures, config.Font.File) + if err != nil { + return nil, fmt.Errorf("invalid font options: %w", err) + } + + // Create SVG formatter + f := formatter.New(fontOptions...) + + // Format to SVG + buf := &bytes.Buffer{} + err = f.Format(buf, style, it) + if err != nil { + return nil, fmt.Errorf("could not format to SVG: %w", err) + } + + // Parse SVG document + doc := etree.NewDocument() + _, err = doc.ReadFrom(buf) + if err != nil { + return nil, fmt.Errorf("could not parse SVG: %w", err) + } + + elements := doc.ChildElements() + if len(elements) < 1 { + return nil, errors.New("invalid SVG output") + } + + image := elements[0] + + // Calculate dimensions + w, h := svg.GetDimensions(image) + imageWidth := float64(w) * scale + imageHeight := float64(h) * scale + + // Adjust for font size and line height + imageHeight *= config.Font.Size / defaultFontSize + imageHeight *= config.LineHeight / defaultLineHeight + + terminalWidth := imageWidth + terminalHeight := imageHeight + + hPadding := expandedPadding[left] + expandedPadding[right] + hMargin := expandedMargin[left] + expandedMargin[right] + vMargin := expandedMargin[top] + expandedMargin[bottom] + vPadding := expandedPadding[top] + expandedPadding[bottom] + + // Calculate final dimensions + if !autoWidth { + imageWidth = config.Width + terminalWidth = config.Width - hMargin + } else { + imageWidth += hMargin + hPadding + terminalWidth += hPadding + } + + if !autoHeight { + imageHeight = config.Height + terminalHeight = config.Height - vMargin + } else { + imageHeight += vMargin + vPadding + terminalHeight += vPadding + } + + // Get terminal background element + terminal := image.SelectElement("rect") + if terminal == nil { + return nil, errors.New("could not find terminal background element") + } + + // Add window controls if enabled + if config.Window { + windowControls := svg.NewWindowControls(5.5*scale, 19.0*scale, 12.0*scale) + svg.Move(windowControls, expandedMargin[left], expandedMargin[top]) + image.AddChild(windowControls) + expandedPadding[top] += 15 * scale + } + + // Add corner radius + if config.Border.Radius > 0 { + svg.AddCornerRadius(terminal, config.Border.Radius*scale) + } + + // Add shadow + if config.Shadow.Blur > 0 || config.Shadow.X > 0 || config.Shadow.Y > 0 { + id := "shadow" + svg.AddShadow(image, id, config.Shadow.X*scale, config.Shadow.Y*scale, config.Shadow.Blur*scale) + terminal.CreateAttr("filter", fmt.Sprintf("url(#%s)", id)) + } + + // Process text elements + textGroup := image.SelectElement("g") + if textGroup != nil { + textGroup.CreateAttr("font-size", fmt.Sprintf("%.2fpx", config.Font.Size*scale)) + textGroup.CreateAttr("clip-path", "url(#terminalMask)") + text := textGroup.SelectElements("text") + + offsetLine := 0 + if len(config.Lines) > 0 { + offsetLine = config.Lines[0] + } + + lineHeight := config.LineHeight * scale + + for i, line := range text { + if isAnsi { + line.SetText("") + } + + // Add line numbers if enabled + if config.ShowLineNumbers { + ln := etree.NewElement("tspan") + ln.CreateAttr("xml:space", "preserve") + ln.CreateAttr("fill", style.Get(chroma.LineNumbers).Colour.String()) + ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine)) + line.InsertChildAt(0, ln) + } + + // Position the line + x := expandedPadding[left] + expandedMargin[left] + y := (float64(i+1))*(config.Font.Size*lineHeight) + expandedPadding[top] + expandedMargin[top] + + svg.Move(line, x, y) + + // Remove lines that are outside the visible area + if y > imageHeight-expandedMargin[bottom]-expandedPadding[bottom] { + textGroup.RemoveChild(line) + } + } + + // Process ANSI sequences if needed + if isAnsi { + processANSI(processedInput, text, textGroup, config, scale) + } + } + + // Calculate auto width based on content + if autoWidth { + tabWidth := 4 + if isAnsi { + tabWidth = 6 + } + strippedInput := ansi.Strip(processedInput) + longestLine := lipgloss.Width(strings.ReplaceAll(strippedInput, "\t", strings.Repeat(" ", tabWidth))) + terminalWidth = float64(longestLine+1) * (config.Font.Size / font.GetFontHeightToWidthRatio()) + terminalWidth *= scale + terminalWidth += hPadding + imageWidth = terminalWidth + hMargin + } + + // Add border + if config.Border.Width > 0 { + svg.AddOutline(terminal, config.Border.Width, config.Border.Color) + terminalHeight -= config.Border.Width * 2 + terminalWidth -= config.Border.Width * 2 + } + + // Adjust for line numbers + if config.ShowLineNumbers { + if autoWidth { + terminalWidth += config.Font.Size * 3 * scale + imageWidth += config.Font.Size * 3 * scale + } else { + terminalWidth -= config.Font.Size * 3 + } + } + + // Add clipping path if needed + if !autoHeight || !autoWidth { + svg.AddClipPath(image, "terminalMask", + expandedMargin[left], expandedMargin[top], + terminalWidth, terminalHeight-expandedPadding[bottom]) + } + + // Set final positions and dimensions + svg.Move(terminal, max(expandedMargin[left], config.Border.Width/2), max(expandedMargin[top], config.Border.Width/2)) + svg.SetDimensions(image, imageWidth, imageHeight) + svg.SetDimensions(terminal, terminalWidth, terminalHeight) + + // Convert to bytes + return doc.WriteToBytes() +} + +// ConvertToPNG converts SVG data to PNG format +func (g *Generator) ConvertToPNG(svgData []byte, width, height float64) ([]byte, error) { + // Parse SVG document + doc := etree.NewDocument() + err := doc.ReadFromBytes(svgData) + if err != nil { + return nil, fmt.Errorf("could not parse SVG: %w", err) + } + + // Use resvg for conversion + worker, err := resvg.NewDefaultWorker(context.Background()) + if err != nil { + return nil, fmt.Errorf("could not create resvg worker: %w", err) + } + defer worker.Close() + + fontdb, err := worker.NewFontDBDefault() + if err != nil { + return nil, fmt.Errorf("could not create font database: %w", err) + } + defer fontdb.Close() + + // Load embedded fonts + if len(font.JetBrainsMonoTTF) > 0 { + err = fontdb.LoadFontData(font.JetBrainsMonoTTF) + if err != nil { + return nil, fmt.Errorf("could not load JetBrains Mono font: %w", err) + } + } + + pixmap, err := worker.NewPixmap(uint32(width), uint32(height)) + if err != nil { + return nil, fmt.Errorf("could not create pixmap: %w", err) + } + defer pixmap.Close() + + tree, err := worker.NewTreeFromData(svgData, &resvg.Options{ + Dpi: 192, + ShapeRenderingMode: resvg.ShapeRenderingModeGeometricPrecision, + TextRenderingMode: resvg.TextRenderingModeOptimizeLegibility, + ImageRenderingMode: resvg.ImageRenderingModeOptimizeQuality, + DefaultSizeWidth: float32(width), + DefaultSizeHeight: float32(height), + }) + if err != nil { + return nil, fmt.Errorf("could not create SVG tree: %w", err) + } + defer tree.Close() + + err = tree.ConvertText(fontdb) + if err != nil { + return nil, fmt.Errorf("could not convert text: %w", err) + } + + err = tree.Render(resvg.TransformIdentity(), pixmap) + if err != nil { + return nil, fmt.Errorf("could not render SVG: %w", err) + } + + pngData, err := pixmap.EncodePNG() + if err != nil { + return nil, fmt.Errorf("could not encode PNG: %w", err) + } + + return pngData, nil +} + +// cutLines cuts the input to the specified line range +func cutLines(input string, lines []int) string { + if len(lines) != 2 { + return input + } + + inputLines := strings.Split(input, "\n") + start := lines[0] + end := lines[1] + + if start < 0 { + start = 0 + } + if end >= len(inputLines) || end < 0 { + end = len(inputLines) - 1 + } + if start > end { + return "" + } + + return strings.Join(inputLines[start:end+1], "\n") +} + +// max returns the maximum of two float64 values +func max(a, b float64) float64 { + if a > b { + return a + } + return b +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ad3925c --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/landaiqing/freezelib + +go 1.23.0 + +toolchain go1.24.3 + +require ( + github.com/alecthomas/chroma/v2 v2.19.0 + github.com/beevik/etree v1.5.1 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.9.3 + github.com/charmbracelet/x/cellbuf v0.0.13 + github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 + github.com/mattn/go-runewidth v0.0.16 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aab6579 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4= +github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo= +github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY= +github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5/go.mod h1:c9+VS9GaommgIOzNWb5ze4lYwfT8BZ2UDyGiuQTT7yc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= +github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/presets.go b/presets.go new file mode 100644 index 0000000..1540c37 --- /dev/null +++ b/presets.go @@ -0,0 +1,194 @@ +package freezelib + +// BasePreset returns a basic configuration for simple code screenshots +func BasePreset() *Config { + config := DefaultConfig() + config.Background = "#171717" + config.SetPadding(20) + config.SetMargin(0) + config.Window = false + config.Border = Border{Radius: 0, Width: 0, Color: "#515151"} + config.Shadow = Shadow{Blur: 0, X: 0, Y: 0} + config.ShowLineNumbers = false + return config +} + +// FullPreset returns a macOS-like configuration with window controls and shadow +func FullPreset() *Config { + config := DefaultConfig() + config.Background = "#282c34" + config.SetPadding(20, 40, 20, 20) + config.SetMargin(20) + config.Window = true + config.Border = Border{Radius: 8, Width: 0, Color: "#515151"} + config.Shadow = Shadow{Blur: 20, X: 0, Y: 10} + config.ShowLineNumbers = false + config.Theme = "github-dark" + return config +} + +// TerminalPreset returns a configuration optimized for terminal output +func TerminalPreset() *Config { + config := DefaultConfig() + config.Background = "#0d1117" + config.SetPadding(15) + config.SetMargin(10) + config.Window = false + config.Border = Border{Radius: 6, Width: 1, Color: "#30363d"} + config.Shadow = Shadow{Blur: 15, X: 0, Y: 5} + config.ShowLineNumbers = false + config.Theme = "github-dark" + config.Font.Family = "JetBrains Mono" + config.Font.Size = 13 + return config +} + +// PresentationPreset returns a configuration suitable for presentations +func PresentationPreset() *Config { + config := DefaultConfig() + config.Background = "#ffffff" + config.SetPadding(40) + config.SetMargin(30) + config.Window = true + config.Border = Border{Radius: 12, Width: 2, Color: "#e1e4e8"} + config.Shadow = Shadow{Blur: 30, X: 0, Y: 15} + config.ShowLineNumbers = true + config.Theme = "github" + config.Font.Size = 16 + config.LineHeight = 1.4 + return config +} + +// MinimalPreset returns a minimal configuration with no decorations +func MinimalPreset() *Config { + config := DefaultConfig() + config.Background = "#ffffff" + config.SetPadding(10) + config.SetMargin(0) + config.Window = false + config.Border = Border{Radius: 0, Width: 0, Color: ""} + config.Shadow = Shadow{Blur: 0, X: 0, Y: 0} + config.ShowLineNumbers = false + config.Theme = "github" + return config +} + +// DarkPreset returns a dark theme configuration +func DarkPreset() *Config { + config := DefaultConfig() + config.Background = "#1e1e1e" + config.SetPadding(25) + config.SetMargin(15) + config.Window = false + config.Border = Border{Radius: 8, Width: 1, Color: "#3c3c3c"} + config.Shadow = Shadow{Blur: 20, X: 0, Y: 8} + config.ShowLineNumbers = false + config.Theme = "dracula" + config.Font.Family = "Fira Code" + config.Font.Size = 14 + config.Font.Ligatures = true + return config +} + +// LightPreset returns a light theme configuration +func LightPreset() *Config { + config := DefaultConfig() + config.Background = "#fafbfc" + config.SetPadding(25) + config.SetMargin(15) + config.Window = false + config.Border = Border{Radius: 8, Width: 1, Color: "#d1d5da"} + config.Shadow = Shadow{Blur: 20, X: 0, Y: 8} + config.ShowLineNumbers = false + config.Theme = "github" + config.Font.Family = "SF Mono" + config.Font.Size = 14 + return config +} + +// RetroPreset returns a retro terminal-style configuration +func RetroPreset() *Config { + config := DefaultConfig() + config.Background = "#000000" + config.SetPadding(20) + config.SetMargin(10) + config.Window = false + config.Border = Border{Radius: 0, Width: 2, Color: "#00ff00"} + config.Shadow = Shadow{Blur: 0, X: 0, Y: 0} + config.ShowLineNumbers = false + config.Theme = "monokai" + config.Font.Family = "Courier New" + config.Font.Size = 12 + config.Font.Ligatures = false + return config +} + +// NeonPreset returns a neon-style configuration +func NeonPreset() *Config { + config := DefaultConfig() + config.Background = "#0a0a0a" + config.SetPadding(30) + config.SetMargin(20) + config.Window = false + config.Border = Border{Radius: 10, Width: 2, Color: "#ff00ff"} + config.Shadow = Shadow{Blur: 25, X: 0, Y: 0} + config.ShowLineNumbers = false + config.Theme = "vim" + config.Font.Family = "Fira Code" + config.Font.Size = 14 + config.Font.Ligatures = true + return config +} + +// CompactPreset returns a compact configuration for small code snippets +func CompactPreset() *Config { + config := DefaultConfig() + config.Background = "#f6f8fa" + config.SetPadding(10) + config.SetMargin(5) + config.Window = false + config.Border = Border{Radius: 4, Width: 1, Color: "#d0d7de"} + config.Shadow = Shadow{Blur: 5, X: 0, Y: 2} + config.ShowLineNumbers = false + config.Theme = "github" + config.Font.Size = 12 + config.LineHeight = 1.1 + return config +} + +// PresetMap contains all available presets +var PresetMap = map[string]func() *Config{ + "base": BasePreset, + "full": FullPreset, + "terminal": TerminalPreset, + "presentation": PresentationPreset, + "minimal": MinimalPreset, + "dark": DarkPreset, + "light": LightPreset, + "retro": RetroPreset, + "neon": NeonPreset, + "compact": CompactPreset, +} + +// GetPreset returns a preset configuration by name +func GetPreset(name string) *Config { + if preset, exists := PresetMap[name]; exists { + return preset() + } + return DefaultConfig() +} + +// ListPresets returns a list of available preset names +func ListPresets() []string { + presets := make([]string, 0, len(PresetMap)) + for name := range PresetMap { + presets = append(presets, name) + } + return presets +} + +// IsValidPreset checks if a preset name is valid +func IsValidPreset(name string) bool { + _, exists := PresetMap[name] + return exists +} diff --git a/quickfreeze.go b/quickfreeze.go new file mode 100644 index 0000000..5a68c6a --- /dev/null +++ b/quickfreeze.go @@ -0,0 +1,348 @@ +package freezelib + +import ( + "fmt" + "strings" +) + +// QuickFreeze provides a simplified, chainable API for quick code screenshots +type QuickFreeze struct { + config *Config +} + +// NewQuickFreeze creates a new QuickFreeze instance with default configuration +func NewQuickFreeze() *QuickFreeze { + return &QuickFreeze{ + config: DefaultConfig(), + } +} + +// NewQuickFreezeWithPreset creates a new QuickFreeze instance with a preset +func NewQuickFreezeWithPreset(presetName string) *QuickFreeze { + return &QuickFreeze{ + config: GetPreset(presetName), + } +} + +// WithTheme sets the syntax highlighting theme +func (qf *QuickFreeze) WithTheme(theme string) *QuickFreeze { + qf.config.SetTheme(theme) + return qf +} + +// WithFont sets the font family and size +func (qf *QuickFreeze) WithFont(family string, size float64) *QuickFreeze { + qf.config.SetFont(family, size) + return qf +} + +// WithBackground sets the background color +func (qf *QuickFreeze) WithBackground(color string) *QuickFreeze { + qf.config.SetBackground(color) + return qf +} + +// WithWindow enables window controls +func (qf *QuickFreeze) WithWindow() *QuickFreeze { + qf.config.SetWindow(true) + return qf +} + +// WithoutWindow disables window controls +func (qf *QuickFreeze) WithoutWindow() *QuickFreeze { + qf.config.SetWindow(false) + return qf +} + +// WithLineNumbers enables line numbers +func (qf *QuickFreeze) WithLineNumbers() *QuickFreeze { + qf.config.SetLineNumbers(true) + return qf +} + +// WithoutLineNumbers disables line numbers +func (qf *QuickFreeze) WithoutLineNumbers() *QuickFreeze { + qf.config.SetLineNumbers(false) + return qf +} + +// WithShadow adds a shadow effect +func (qf *QuickFreeze) WithShadow() *QuickFreeze { + qf.config.SetShadow(20, 0, 10) + return qf +} + +// WithCustomShadow adds a custom shadow effect +func (qf *QuickFreeze) WithCustomShadow(blur, x, y float64) *QuickFreeze { + qf.config.SetShadow(blur, x, y) + return qf +} + +// WithoutShadow removes shadow effect +func (qf *QuickFreeze) WithoutShadow() *QuickFreeze { + qf.config.SetShadow(0, 0, 0) + return qf +} + +// WithBorder adds a border +func (qf *QuickFreeze) WithBorder() *QuickFreeze { + qf.config.SetBorder(1, 8, "#515151") + return qf +} + +// WithCustomBorder adds a custom border +func (qf *QuickFreeze) WithCustomBorder(width, radius float64, color string) *QuickFreeze { + qf.config.SetBorder(width, radius, color) + return qf +} + +// WithoutBorder removes border +func (qf *QuickFreeze) WithoutBorder() *QuickFreeze { + qf.config.SetBorder(0, 0, "") + return qf +} + +// WithPadding sets padding (1, 2, or 4 values like CSS) +func (qf *QuickFreeze) WithPadding(values ...float64) *QuickFreeze { + qf.config.SetPadding(values...) + return qf +} + +// WithMargin sets margin (1, 2, or 4 values like CSS) +func (qf *QuickFreeze) WithMargin(values ...float64) *QuickFreeze { + qf.config.SetMargin(values...) + return qf +} + +// WithDimensions sets specific width and height +func (qf *QuickFreeze) WithDimensions(width, height float64) *QuickFreeze { + qf.config.SetDimensions(width, height) + return qf +} + +// WithWidth sets specific width (height auto) +func (qf *QuickFreeze) WithWidth(width float64) *QuickFreeze { + qf.config.Width = width + return qf +} + +// WithHeight sets specific height (width auto) +func (qf *QuickFreeze) WithHeight(height float64) *QuickFreeze { + qf.config.Height = height + return qf +} + +// WithLines sets the line range to capture (1-indexed) +func (qf *QuickFreeze) WithLines(start, end int) *QuickFreeze { + qf.config.SetLines(start, end) + return qf +} + +// WithLanguage sets the programming language for syntax highlighting +func (qf *QuickFreeze) WithLanguage(language string) *QuickFreeze { + qf.config.SetLanguage(language) + return qf +} + +// WithLineHeight sets the line height ratio +func (qf *QuickFreeze) WithLineHeight(ratio float64) *QuickFreeze { + qf.config.LineHeight = ratio + return qf +} + +// WithWrap sets text wrapping at specified column +func (qf *QuickFreeze) WithWrap(columns int) *QuickFreeze { + qf.config.Wrap = columns + return qf +} + +// CodeToSVG generates SVG from source code +func (qf *QuickFreeze) CodeToSVG(code string) ([]byte, error) { + generator := NewGenerator(qf.config) + return generator.GenerateFromCode(code, qf.config.Language) +} + +// CodeToPNG generates PNG from source code +func (qf *QuickFreeze) CodeToPNG(code string) ([]byte, error) { + generator := NewGenerator(qf.config) + svgData, err := generator.GenerateFromCode(code, qf.config.Language) + if err != nil { + return nil, err + } + + 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) +} + +// FileToSVG generates SVG from a source code file +func (qf *QuickFreeze) FileToSVG(filename string) ([]byte, error) { + generator := NewGenerator(qf.config) + return generator.GenerateFromFile(filename) +} + +// FileToPNG generates PNG from a source code file +func (qf *QuickFreeze) FileToPNG(filename string) ([]byte, error) { + generator := NewGenerator(qf.config) + svgData, err := generator.GenerateFromFile(filename) + if err != nil { + return nil, err + } + + 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) +} + +// ANSIToSVG generates SVG from ANSI terminal output +func (qf *QuickFreeze) ANSIToSVG(ansiOutput string) ([]byte, error) { + generator := NewGenerator(qf.config) + return generator.GenerateFromANSI(ansiOutput) +} + +// ANSIToPNG generates PNG from ANSI terminal output +func (qf *QuickFreeze) ANSIToPNG(ansiOutput string) ([]byte, error) { + generator := NewGenerator(qf.config) + svgData, err := generator.GenerateFromANSI(ansiOutput) + if err != nil { + return nil, err + } + + 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) +} + +// SaveCodeToFile generates and saves code screenshot to file +func (qf *QuickFreeze) SaveCodeToFile(code, filename string) error { + var data []byte + var err error + + if isPNGFile(filename) { + data, err = qf.CodeToPNG(code) + } else { + data, err = qf.CodeToSVG(code) + } + + if err != nil { + return err + } + + return saveToFile(data, filename) +} + +// SaveFileToFile generates and saves file screenshot to file +func (qf *QuickFreeze) SaveFileToFile(inputFile, outputFile string) error { + var data []byte + var err error + + if isPNGFile(outputFile) { + data, err = qf.FileToPNG(inputFile) + } else { + data, err = qf.FileToSVG(inputFile) + } + + if err != nil { + return err + } + + return saveToFile(data, outputFile) +} + +// SaveANSIToFile generates and saves ANSI screenshot to file +func (qf *QuickFreeze) SaveANSIToFile(ansiOutput, filename string) error { + var data []byte + var err error + + if isPNGFile(filename) { + data, err = qf.ANSIToPNG(ansiOutput) + } else { + data, err = qf.ANSIToSVG(ansiOutput) + } + + if err != nil { + return err + } + + return saveToFile(data, filename) +} + +// Config returns the current configuration +func (qf *QuickFreeze) Config() *Config { + return qf.config +} + +// Clone creates a copy of the QuickFreeze instance +func (qf *QuickFreeze) Clone() *QuickFreeze { + return &QuickFreeze{ + config: qf.config.Clone(), + } +} + +// Reset resets the configuration to defaults +func (qf *QuickFreeze) Reset() *QuickFreeze { + qf.config = DefaultConfig() + return qf +} + +// ResetToPreset resets the configuration to a specific preset +func (qf *QuickFreeze) ResetToPreset(presetName string) *QuickFreeze { + qf.config = GetPreset(presetName) + return qf +} + +// String returns a string representation of the current configuration +func (qf *QuickFreeze) String() string { + var parts []string + + parts = append(parts, fmt.Sprintf("Theme: %s", qf.config.Theme)) + parts = append(parts, fmt.Sprintf("Font: %s %.1fpx", qf.config.Font.Family, qf.config.Font.Size)) + parts = append(parts, fmt.Sprintf("Background: %s", qf.config.Background)) + + if qf.config.Window { + parts = append(parts, "Window: enabled") + } + + if qf.config.ShowLineNumbers { + parts = append(parts, "Line numbers: enabled") + } + + if qf.config.Shadow.Blur > 0 { + parts = append(parts, fmt.Sprintf("Shadow: blur=%.1f", qf.config.Shadow.Blur)) + } + + if qf.config.Border.Width > 0 { + parts = append(parts, fmt.Sprintf("Border: width=%.1f", qf.config.Border.Width)) + } + + return "QuickFreeze{" + strings.Join(parts, ", ") + "}" +} + +// saveToFile is a helper function to save data to file +func saveToFile(data []byte, filename string) error { + return NewWithConfig(DefaultConfig()).SaveToFile(data, filename) +} diff --git a/sample/basic_example.svg b/sample/basic_example.svg new file mode 100644 index 0000000..6373887 --- /dev/null +++ b/sample/basic_example.svg @@ -0,0 +1,15 @@ + + + + + +package main + +import "fmt" + +func main() { +    fmt.Println("Hello, World!") +    fmt.Println("This is a beautiful code screenshot!") +} + + diff --git a/sample/chained_example.svg b/sample/chained_example.svg new file mode 100644 index 0000000..680d89e --- /dev/null +++ b/sample/chained_example.svg @@ -0,0 +1,24 @@ + + + + + + 1 #include <iostream> + 2 #include <vector> + 3 #include <algorithm> + 4 + 5 int main() { + 6     std::vector<int> numbers = {52819}; + 7      + 8     std::sort(numbers.begin(), numbers.end()); + 9      + 10     std::cout << "Sorted numbers: "; + 11     for (const auto& num : numbers) { + 12         std::cout << num << " "; + 13     } + 14     std::cout << std::endl; + 15      + 16     return 0; + 17 } + + diff --git a/sample/custom_config_example.svg b/sample/custom_config_example.svg new file mode 100644 index 0000000..5240174 --- /dev/null +++ b/sample/custom_config_example.svg @@ -0,0 +1,25 @@ + + + + + + 1 import numpy as np + 2 import matplotlib.pyplot as plt + 3 + 4 def plot_sine_wave(): + 5     x = np.linspace(0, 2 * np.pi, 100) + 6     y = np.sin(x) + 7      + 8     plt.figure(figsize=(10, 6)) + 9     plt.plot(x, y, 'b-', linewidth=2, label='sin(x)') + 10     plt.xlabel('x') + 11     plt.ylabel('sin(x)') + 12     plt.title('Sine Wave') + 13     plt.grid(True, alpha=0.3) + 14     plt.legend() + 15     plt.show() + 16 + 17 if __name__ == "__main__": + 18     plot_sine_wave() + + diff --git a/sample/file_example.svg b/sample/file_example.svg new file mode 100644 index 0000000..34aa56e --- /dev/null +++ b/sample/file_example.svg @@ -0,0 +1,19 @@ + + + + + + 1 use std::collections::HashMap; + 2 + 3 fn main() { + 4     let mut scores = HashMap::new(); + 5      + 6     scores.insert(String::from("Blue"), 10); + 7     scores.insert(String::from("Yellow"), 50); + 8      + 9     for (key, value) in &scores { + 10         println!("{}{}", key, value); + 11     } + 12 } + + diff --git a/sample/main.go b/sample/main.go new file mode 100644 index 0000000..a81e36b --- /dev/null +++ b/sample/main.go @@ -0,0 +1,329 @@ +package main + +import ( + "fmt" + "os" + + "github.com/landaiqing/freezelib" +) + +func main() { + fmt.Println("🎨 Freeze Library Examples") + fmt.Println("========================") + + // Run all examples + basicExample() + quickFreezeExample() + terminalExample() + customConfigExample() + fileExample() + presetExample() + chainedExample() + + fmt.Println("\n✅ All examples completed successfully!") + fmt.Println("Check the generated files in the current directory.") +} + +func basicExample() { + fmt.Println("\n📝 Basic Example") + fmt.Println("----------------") + + // Create a new freeze instance + freeze := freezelib.New() + + // Go code to screenshot + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") + fmt.Println("This is a beautiful code screenshot!") +}` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + // Save to file + err = os.WriteFile("basic_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated basic_example.svg") +} + +func quickFreezeExample() { + fmt.Println("\n⚡ QuickFreeze Example") + fmt.Println("---------------------") + + // Use QuickFreeze for simplified API + qf := freezelib.NewQuickFreeze() + + // JavaScript code with styling + code := `function fibonacci(n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +console.log('Fibonacci sequence:'); +for (let i = 0; i < 10; i++) { + console.log('F(' + i + ') = ' + fibonacci(i)); +}` + + // Chain styling options + svgData, err := qf.WithTheme("dracula"). + WithFont("Fira Code", 14). + WithWindow(). + WithShadow(). + WithLineNumbers(). + WithLanguage("javascript"). + CodeToSVG(code) + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + err = os.WriteFile("quickfreeze_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated quickfreeze_example.svg") +} + +func terminalExample() { + fmt.Println("\n💻 Terminal Example") + fmt.Println("-------------------") + + // Use terminal preset for ANSI output + freeze := freezelib.NewWithConfig(freezelib.TerminalPreset()) + + // Colored terminal output + terminalOutput := "\033[32m✓ SUCCESS\033[0m: Build completed successfully\n" + + "\033[33m⚠ WARNING\033[0m: Deprecated function used in main.go:42\n" + + "\033[31m✗ ERROR\033[0m: File not found: config.json\n" + + "\033[36mINFO\033[0m: Starting server on port 8080\n" + + "\033[35mDEBUG\033[0m: Loading configuration from ~/.config/app" + + svgData, err := freeze.GenerateFromANSI(terminalOutput) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + err = os.WriteFile("terminal_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated terminal_example.svg") +} + +func customConfigExample() { + fmt.Println("\n⚙️ Custom Config Example") + fmt.Println("-------------------------") + + // Create custom configuration + config := freezelib.DefaultConfig() + + // Customize appearance + config.Theme = "github" + config.Background = "#f6f8fa" + config.Font.Family = "JetBrains Mono" + config.Font.Size = 16 + config.LineHeight = 1.4 + + // Layout settings + config.SetPadding(30) + config.SetMargin(20) + config.Width = 800 + + // Effects + config.Window = true + config.ShowLineNumbers = true + config.Border.Radius = 12 + config.Border.Width = 2 + config.Border.Color = "#d1d9e0" + config.Shadow.Blur = 25 + config.Shadow.Y = 15 + + // Create freeze instance with custom config + freeze := freezelib.NewWithConfig(config) + + // Python code + code := `import numpy as np +import matplotlib.pyplot as plt + +def plot_sine_wave(): + x = np.linspace(0, 2 * np.pi, 100) + y = np.sin(x) + + plt.figure(figsize=(10, 6)) + plt.plot(x, y, 'b-', linewidth=2, label='sin(x)') + plt.xlabel('x') + plt.ylabel('sin(x)') + plt.title('Sine Wave') + plt.grid(True, alpha=0.3) + plt.legend() + plt.show() + +if __name__ == "__main__": + plot_sine_wave()` + + svgData, err := freeze.GenerateFromCode(code, "python") + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + err = os.WriteFile("custom_config_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated custom_config_example.svg") +} + +func fileExample() { + fmt.Println("\n📁 File Example") + fmt.Println("---------------") + + // Create a sample Rust file + sampleCode := `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); + } +}` + + // Create sample file + err := os.WriteFile("sample.rs", []byte(sampleCode), 0644) + if err != nil { + fmt.Printf("Error creating sample file: %v\n", err) + return + } + + // Use presentation preset + freeze := freezelib.NewWithConfig(freezelib.PresentationPreset()) + + // Generate from file + svgData, err := freeze.GenerateFromFile("sample.rs") + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + err = os.WriteFile("file_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated file_example.svg") + fmt.Println("✓ Created sample.rs") +} + +func presetExample() { + fmt.Println("\n🎨 Preset Example") + fmt.Println("-----------------") + + code := `const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { + res.json({ message: 'Hello, World!' }); +}); + +app.listen(3000, () => { + console.log('Server running on port 3000'); +});` + + // Try different presets + presets := []string{"dark", "light", "minimal", "retro"} + + for _, preset := range presets { + freeze := freezelib.NewWithPreset(preset) + svgData, err := freeze.GenerateFromCode(code, "javascript") + if err != nil { + fmt.Printf("Error with preset %s: %v\n", preset, err) + continue + } + + filename := fmt.Sprintf("preset_%s_example.svg", preset) + 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) + } +} + +func chainedExample() { + fmt.Println("\n🔗 Chained Methods Example") + fmt.Println("---------------------------") + + // Create base freeze instance + freeze := freezelib.New() + + code := `#include +#include +#include + +int main() { + std::vector numbers = {5, 2, 8, 1, 9}; + + std::sort(numbers.begin(), numbers.end()); + + std::cout << "Sorted numbers: "; + for (const auto& num : numbers) { + std::cout << num << " "; + } + std::cout << std::endl; + + return 0; +}` + + // Chain multiple styling methods + svgData, err := freeze. + WithTheme("monokai"). + WithFont("Cascadia Code", 15). + WithBackground("#2d2d2d"). + WithWindow(true). + WithLineNumbers(true). + WithShadow(20, 0, 10). + WithBorder(1, 10, "#444444"). + WithPadding(25). + WithMargin(15). + GenerateFromCode(code, "cpp") + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + err = os.WriteFile("chained_example.svg", svgData, 0644) + if err != nil { + fmt.Printf("Error saving file: %v\n", err) + return + } + + fmt.Println("✓ Generated chained_example.svg") +} diff --git a/sample/preset_dark_example.svg b/sample/preset_dark_example.svg new file mode 100644 index 0000000..c375c96 --- /dev/null +++ b/sample/preset_dark_example.svg @@ -0,0 +1,17 @@ + + + + + +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { +  res.json({ message: 'Hello, World!' }); +}); + +app.listen(3000, () => { +  console.log('Server running on port 3000'); +}); + + diff --git a/sample/preset_light_example.svg b/sample/preset_light_example.svg new file mode 100644 index 0000000..a0cbb7c --- /dev/null +++ b/sample/preset_light_example.svg @@ -0,0 +1,17 @@ + + + + + +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { +  res.json({ message: 'Hello, World!' }); +}); + +app.listen(3000, () => { +  console.log('Server running on port 3000'); +}); + + diff --git a/sample/preset_minimal_example.svg b/sample/preset_minimal_example.svg new file mode 100644 index 0000000..26f5e67 --- /dev/null +++ b/sample/preset_minimal_example.svg @@ -0,0 +1,17 @@ + + + + + +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { +  res.json({ message: 'Hello, World!' }); +}); + +app.listen(3000, () => { +  console.log('Server running on port 3000'); +}); + + diff --git a/sample/preset_retro_example.svg b/sample/preset_retro_example.svg new file mode 100644 index 0000000..c0c09e3 --- /dev/null +++ b/sample/preset_retro_example.svg @@ -0,0 +1,17 @@ + + + + + +const express = require('express'); +const app = express(); + +app.get('/', (reqres) => { +  res.json({ message: 'Hello, World!' }); +}); + +app.listen(3000, () => { +  console.log('Server running on port 3000'); +}); + + diff --git a/sample/quickfreeze_example.svg b/sample/quickfreeze_example.svg new file mode 100644 index 0000000..d76d7a2 --- /dev/null +++ b/sample/quickfreeze_example.svg @@ -0,0 +1,16 @@ + + + + + + 1 function fibonacci(n) { + 2     if (n <= 1return n; + 3     return fibonacci(n - 1+ fibonacci(n - 2); + 4 } + 5 + 6 console.log('Fibonacci sequence:'); + 7 for (let i = 0; i < 10; i++) { + 8     console.log('F(' + i + ') = ' + fibonacci(i)); + 9 } + + diff --git a/sample/sample.rs b/sample/sample.rs new file mode 100644 index 0000000..fa2b37e --- /dev/null +++ b/sample/sample.rs @@ -0,0 +1,12 @@ +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); + } +} \ No newline at end of file diff --git a/sample/simple_test.go b/sample/simple_test.go new file mode 100644 index 0000000..d56f793 --- /dev/null +++ b/sample/simple_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "github.com/landaiqing/freezelib" + "os" +) + +func simpleTest() { + fmt.Println("🧪 Simple Test") + fmt.Println("==============") + + // Create a new freeze instance + freeze := freezelib.New() + + // Simple Go code to test + code := `package main + +import "fmt" + +func main() { + fmt.Println("Hello from FreezeLib!") +}` + + // Generate SVG + svgData, err := freeze.GenerateFromCode(code, "go") + if err != nil { + fmt.Printf("❌ Error: %v\n", err) + return + } + + // Save to file + err = os.WriteFile("simple_test.svg", svgData, 0644) + if err != nil { + fmt.Printf("❌ Error saving file: %v\n", err) + return + } + + fmt.Printf("✅ Generated simple_test.svg (%d bytes)\n", len(svgData)) + + // Test QuickFreeze API + qf := freezelib.NewQuickFreeze() + svgData2, err := qf.WithTheme("github").CodeToSVG(code) + if err != nil { + fmt.Printf("❌ QuickFreeze Error: %v\n", err) + return + } + + err = os.WriteFile("quickfreeze_test.svg", svgData2, 0644) + if err != nil { + fmt.Printf("❌ Error saving QuickFreeze file: %v\n", err) + return + } + + fmt.Printf("✅ Generated quickfreeze_test.svg (%d bytes)\n", len(svgData2)) + + // Test ANSI output + ansiOutput := "\033[32m✓ SUCCESS\033[0m: Test passed\n\033[31m✗ ERROR\033[0m: Test failed" + ansiData, err := freeze.GenerateFromANSI(ansiOutput) + if err != nil { + fmt.Printf("❌ ANSI Error: %v\n", err) + return + } + + err = os.WriteFile("ansi_test.svg", ansiData, 0644) + if err != nil { + fmt.Printf("❌ Error saving ANSI file: %v\n", err) + return + } + + fmt.Printf("✅ Generated ansi_test.svg (%d bytes)\n", len(ansiData)) + fmt.Println("🎉 All tests passed!") +} + +func init() { + // Run simple test instead of full examples + if len(os.Args) > 1 && os.Args[1] == "test" { + simpleTest() + os.Exit(0) + } +} diff --git a/sample/terminal_example.svg b/sample/terminal_example.svg new file mode 100644 index 0000000..7df700d --- /dev/null +++ b/sample/terminal_example.svg @@ -0,0 +1,8 @@ + + + + + +✓ SUCCESS: Build completed successfully⚠ WARNING: Deprecated function used in main.go:42✗ ERROR: File not found: config.jsonINFO: Starting server on port 8080DEBUG: Loading configuration from ~/.config/app + + diff --git a/svg/svg.go b/svg/svg.go new file mode 100644 index 0000000..05b7aa6 --- /dev/null +++ b/svg/svg.go @@ -0,0 +1,220 @@ +package svg + +import ( + "fmt" + "strconv" + "strings" + + "github.com/beevik/etree" +) + +// AddShadow adds a definition of a shadow to the with the given id. +func AddShadow(element *etree.Element, id string, x, y, blur float64) { + f := etree.NewElement("filter") + f.CreateAttr("id", id) + f.CreateAttr("filterUnits", "userSpaceOnUse") + + b := etree.NewElement("feGaussianBlur") + b.CreateAttr("in", "SourceAlpha") + b.CreateAttr("stdDeviation", fmt.Sprintf("%.2f", blur)) + + o := etree.NewElement("feOffset") + o.CreateAttr("result", "offsetblur") + o.CreateAttr("dx", fmt.Sprintf("%.2f", x)) + o.CreateAttr("dy", fmt.Sprintf("%.2f", y)) + + m := etree.NewElement("feMerge") + mn1 := etree.NewElement("feMergeNode") + mn2 := etree.NewElement("feMergeNode") + mn2.CreateAttr("in", "SourceGraphic") + m.AddChild(mn1) + m.AddChild(mn2) + + f.AddChild(b) + f.AddChild(o) + f.AddChild(m) + + defs := etree.NewElement("defs") + defs.AddChild(f) + element.AddChild(defs) +} + +// AddClipPath adds a definition of a clip path to the with the given id. +func AddClipPath(element *etree.Element, id string, x, y, w, h float64) { + p := etree.NewElement("clipPath") + p.CreateAttr("id", id) + + rect := etree.NewElement("rect") + rect.CreateAttr("x", fmt.Sprintf("%.2f", x)) + rect.CreateAttr("y", fmt.Sprintf("%.2f", y)) + rect.CreateAttr("width", fmt.Sprintf("%.2f", w)) + rect.CreateAttr("height", fmt.Sprintf("%.2f", h)) + + p.AddChild(rect) + + defs := etree.NewElement("defs") + defs.AddChild(p) + element.AddChild(defs) +} + +// AddCornerRadius adds corner radius to an element. +func AddCornerRadius(e *etree.Element, radius float64) { + e.CreateAttr("rx", fmt.Sprintf("%.2f", radius)) + e.CreateAttr("ry", fmt.Sprintf("%.2f", radius)) +} + +// Move moves the given element to the (x, y) position. +func Move(e *etree.Element, x, y float64) { + e.CreateAttr("x", fmt.Sprintf("%.2fpx", x)) + e.CreateAttr("y", fmt.Sprintf("%.2fpx", y)) +} + +// AddOutline adds an outline to the given element. +func AddOutline(e *etree.Element, width float64, color string) { + e.CreateAttr("stroke", color) + e.CreateAttr("stroke-width", fmt.Sprintf("%.2f", width)) +} + +const ( + red string = "#FF5A54" + yellow string = "#E6BF29" + green string = "#52C12B" +) + +// NewWindowControls returns a colorful window bar element. +func NewWindowControls(r float64, x, y float64) *etree.Element { + bar := etree.NewElement("svg") + for i, color := range []string{red, yellow, green} { + circle := etree.NewElement("circle") + circle.CreateAttr("cx", fmt.Sprintf("%.2f", float64(i+1)*float64(x)-float64(r))) + circle.CreateAttr("cy", fmt.Sprintf("%.2f", y)) + circle.CreateAttr("r", fmt.Sprintf("%.2f", r)) + circle.CreateAttr("fill", color) + bar.AddChild(circle) + } + return bar +} + +// SetDimensions sets the width and height of the given element. +func SetDimensions(element *etree.Element, width, height float64) { + widthAttr := element.SelectAttr("width") + heightAttr := element.SelectAttr("height") + if heightAttr != nil { + heightAttr.Value = fmt.Sprintf("%.2f", height) + } + if widthAttr != nil { + widthAttr.Value = fmt.Sprintf("%.2f", width) + } +} + +// GetDimensions returns the width and height of the element. +func GetDimensions(element *etree.Element) (int, int) { + widthValue := element.SelectAttrValue("width", "0px") + heightValue := element.SelectAttrValue("height", "0px") + width := dimensionToInt(widthValue) + height := dimensionToInt(heightValue) + return width, height +} + +// dimensionToInt converts dimension strings to integers +func dimensionToInt(dimension string) int { + dimension = strings.TrimSuffix(dimension, "px") + val, err := strconv.Atoi(dimension) + if err != nil { + return 0 + } + return val +} + +// CreateSVGElement creates a new SVG root element with basic attributes +func CreateSVGElement(width, height float64) *etree.Element { + svg := etree.NewElement("svg") + svg.CreateAttr("xmlns", "http://www.w3.org/2000/svg") + svg.CreateAttr("width", fmt.Sprintf("%.2f", width)) + svg.CreateAttr("height", fmt.Sprintf("%.2f", height)) + svg.CreateAttr("viewBox", fmt.Sprintf("0 0 %.2f %.2f", width, height)) + return svg +} + +// CreateRect creates a rectangle element +func CreateRect(x, y, width, height float64, fill string) *etree.Element { + rect := etree.NewElement("rect") + rect.CreateAttr("x", fmt.Sprintf("%.2f", x)) + rect.CreateAttr("y", fmt.Sprintf("%.2f", y)) + rect.CreateAttr("width", fmt.Sprintf("%.2f", width)) + rect.CreateAttr("height", fmt.Sprintf("%.2f", height)) + if fill != "" { + rect.CreateAttr("fill", fill) + } + return rect +} + +// CreateText creates a text element +func CreateText(x, y float64, content string) *etree.Element { + text := etree.NewElement("text") + text.CreateAttr("x", fmt.Sprintf("%.2f", x)) + text.CreateAttr("y", fmt.Sprintf("%.2f", y)) + text.SetText(content) + return text +} + +// CreateGroup creates a group element +func CreateGroup() *etree.Element { + return etree.NewElement("g") +} + +// SetFontAttributes sets font-related attributes on an element +func SetFontAttributes(element *etree.Element, family string, size float64) { + if family != "" { + element.CreateAttr("font-family", family) + } + if size > 0 { + element.CreateAttr("font-size", fmt.Sprintf("%.2fpx", size)) + } +} + +// SetTextAttributes sets text-related attributes +func SetTextAttributes(element *etree.Element, fill, textAnchor string) { + if fill != "" { + element.CreateAttr("fill", fill) + } + if textAnchor != "" { + element.CreateAttr("text-anchor", textAnchor) + } +} + +// AddStyle adds a style attribute to an element +func AddStyle(element *etree.Element, style string) { + existing := element.SelectAttrValue("style", "") + if existing != "" { + style = existing + "; " + style + } + element.CreateAttr("style", style) +} + +// Max returns the maximum of two float64 values +func Max(a, b float64) float64 { + if a > b { + return a + } + return b +} + +// Min returns the minimum of two float64 values +func Min(a, b float64) float64 { + if a < b { + return a + } + return b +} + +// Clamp constrains a value between min and max +func Clamp(value, min, max float64) float64 { + if value < min { + return min + } + if value > max { + return max + } + return value +}