184 lines
6.4 KiB
Text
184 lines
6.4 KiB
Text
|
|
#!/usr/bin/env nu
|
||
|
|
# SVG optimization utilities
|
||
|
|
|
||
|
|
# Optimize SVG content (basic optimization without external tools)
|
||
|
|
export def optimize_svg_content [svg_content: string, config: record] {
|
||
|
|
let optimized = ($svg_content
|
||
|
|
# Remove XML comments
|
||
|
|
| str replace -ar '<!--[\s\S]*?-->' ''
|
||
|
|
# Remove unnecessary whitespace between tags
|
||
|
|
| str replace -ar '>\s+<' '><'
|
||
|
|
# Remove newlines and extra spaces
|
||
|
|
| str replace -ar '\n\s*' ''
|
||
|
|
| str replace -ar '\s+' ' '
|
||
|
|
# Remove unnecessary attributes (if safe)
|
||
|
|
| str replace -ar '\s+xmlns:xlink="[^"]*"' ''
|
||
|
|
# Round numbers to reduce precision (basic)
|
||
|
|
)
|
||
|
|
let rounded = (round_svg_numbers $optimized $config.svg_precision)
|
||
|
|
let final = ($rounded | str trim)
|
||
|
|
|
||
|
|
$final
|
||
|
|
}
|
||
|
|
|
||
|
|
# Round SVG numeric values to reduce file size
|
||
|
|
def round_svg_numbers [content: string, precision: int] {
|
||
|
|
# Basic SVG number optimization using simple regex
|
||
|
|
# Round decimal numbers to specified precision
|
||
|
|
$content
|
||
|
|
| str replace -ar '(\d+\.\d{4,})' '$1' # Keep numbers as-is for now
|
||
|
|
| str replace -ar '\s+' ' ' # Remove extra whitespace
|
||
|
|
| str trim
|
||
|
|
}
|
||
|
|
|
||
|
|
# Convert SVG to data URI
|
||
|
|
export def svg_to_data_uri [svg_content: string] {
|
||
|
|
let cleaned = ($svg_content
|
||
|
|
# Remove newlines and extra spaces
|
||
|
|
| str replace -ar '\n\s*' ' '
|
||
|
|
| str replace -ar '\s+' ' '
|
||
|
|
# URL encode special characters
|
||
|
|
| str replace -ar '"' '%22'
|
||
|
|
| str replace -ar '<' '%3C'
|
||
|
|
| str replace -ar '>' '%3E'
|
||
|
|
| str replace -ar '#' '%23'
|
||
|
|
| str trim
|
||
|
|
)
|
||
|
|
|
||
|
|
$"data:image/svg+xml,%3Csvg ($cleaned | str substring 4..)"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Extract symbols from SVG sprite
|
||
|
|
export def extract_svg_symbols [svg_content: string] {
|
||
|
|
# Extract symbol definitions using regex parsing
|
||
|
|
let symbol_matches = ($svg_content | parse -r '<symbol[^>]*id="([^"]*)"[^>]*(?:viewBox="([^"]*)")?[^>]*>(.*?)</symbol>')
|
||
|
|
|
||
|
|
$symbol_matches | each { |match|
|
||
|
|
{
|
||
|
|
id: $match.capture0,
|
||
|
|
content: $match.capture2,
|
||
|
|
viewBox: (if ($match.capture1 | is-empty) { "0 0 24 24" } else { $match.capture1 })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create optimized SVG sprite
|
||
|
|
export def create_svg_sprite [symbols: list<record>, config: record] {
|
||
|
|
let sprite_content = ($symbols
|
||
|
|
| each { |symbol|
|
||
|
|
$'<symbol id="($symbol.id)" viewBox="($symbol.viewBox // "0 0 24 24")">($symbol.content)</symbol>'
|
||
|
|
}
|
||
|
|
| str join "\n"
|
||
|
|
)
|
||
|
|
|
||
|
|
let full_sprite = $'<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||
|
|
($sprite_content)
|
||
|
|
</svg>'
|
||
|
|
|
||
|
|
if $config.svg_optimize {
|
||
|
|
optimize_svg_content $full_sprite $config
|
||
|
|
} else {
|
||
|
|
$full_sprite
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Analyze SVG for optimization opportunities
|
||
|
|
export def analyze_svg [svg_content: string] {
|
||
|
|
let size = ($svg_content | str length)
|
||
|
|
let lines = ($svg_content | lines | length)
|
||
|
|
let path_elements = ($svg_content | str replace -ar '<path[^>]*>' '' | str length)
|
||
|
|
let path_count = (($size - $path_elements) / 50) # Rough estimate based on size difference
|
||
|
|
|
||
|
|
{
|
||
|
|
size_bytes: $size,
|
||
|
|
estimated_elements: $lines,
|
||
|
|
estimated_paths: $path_count,
|
||
|
|
has_animations: ($svg_content | str contains "animate"),
|
||
|
|
has_gradients: ($svg_content | str contains "gradient"),
|
||
|
|
has_filters: ($svg_content | str contains "filter"),
|
||
|
|
compression_potential: (($size * 0.4) | math round)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Split SVG into critical and non-critical based on usage
|
||
|
|
export def categorize_svg_symbols [svg_content: string] {
|
||
|
|
let all_symbols = (extract_svg_symbols $svg_content)
|
||
|
|
|
||
|
|
# Define critical symbols (above-the-fold, animated)
|
||
|
|
let critical_ids = ["logo-black", "logo-text-black"]
|
||
|
|
|
||
|
|
let critical_symbols = ($all_symbols | where $it.id in $critical_ids)
|
||
|
|
let decorative_symbols = ($all_symbols | where $it.id not-in $critical_ids)
|
||
|
|
|
||
|
|
{
|
||
|
|
critical: $critical_symbols,
|
||
|
|
decorative: $decorative_symbols
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate CSS classes for SVG symbols
|
||
|
|
export def generate_svg_css_classes [symbols: list<record>] {
|
||
|
|
# Generate CSS classes for each symbol
|
||
|
|
$symbols | each { |symbol|
|
||
|
|
let class_name = ($symbol.id | str replace -ar '[^a-zA-Z0-9]' '-')
|
||
|
|
let svg_data = $"<svg viewBox=\"($symbol.viewBox)\">($symbol.content)</svg>"
|
||
|
|
let encoded = ($svg_data | str replace '"' "'" | str replace '#' '%23')
|
||
|
|
$".icon-($class_name) { background-image: url('data:image/svg+xml;utf8,($encoded)'); }"
|
||
|
|
} | str join "\n"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Optimize SVG animations for performance
|
||
|
|
export def optimize_svg_animations [svg_content: string] {
|
||
|
|
# Basic optimization - remove unnecessary animation attributes
|
||
|
|
let optimized = ($svg_content
|
||
|
|
# Remove animation debugging attributes
|
||
|
|
| str replace -ar '\s+calcMode="[^"]*"' ''
|
||
|
|
| str replace -ar '\s+keyTimes="[^"]*"' ''
|
||
|
|
# Simplify animation values where possible
|
||
|
|
| str replace -ar 'repeatCount="indefinite"' 'repeatCount="indefinite"'
|
||
|
|
)
|
||
|
|
|
||
|
|
$optimized
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create fallback PNG data for SVG (requires external tool)
|
||
|
|
export def create_svg_fallback [svg_content: string, width: int = 24, height: int = 24] {
|
||
|
|
# This would require an external tool like Inkscape or rsvg-convert
|
||
|
|
# For now, return a placeholder
|
||
|
|
"# SVG fallback generation requires external tools (rsvg-convert, inkscape)"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validate SVG syntax
|
||
|
|
export def validate_svg [svg_content: string] {
|
||
|
|
let has_svg_tag = ($svg_content | str contains "<svg")
|
||
|
|
let has_closing_tag = ($svg_content | str contains "</svg>")
|
||
|
|
let balanced_tags = true # This would need proper XML parsing
|
||
|
|
|
||
|
|
{
|
||
|
|
valid: ($has_svg_tag and $has_closing_tag and $balanced_tags),
|
||
|
|
has_svg_root: $has_svg_tag,
|
||
|
|
has_closing_tag: $has_closing_tag,
|
||
|
|
estimated_valid: $balanced_tags
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate SVG performance report
|
||
|
|
export def svg_performance_report [svg_files: list<string>] {
|
||
|
|
$svg_files | each { |file|
|
||
|
|
if ($file | path exists) {
|
||
|
|
let content = (open $file)
|
||
|
|
let analysis = (analyze_svg $content)
|
||
|
|
let validation = (validate_svg $content)
|
||
|
|
|
||
|
|
{
|
||
|
|
file: $file,
|
||
|
|
size: $analysis.size_bytes,
|
||
|
|
elements: $analysis.estimated_elements,
|
||
|
|
paths: $analysis.estimated_paths,
|
||
|
|
animations: $analysis.has_animations,
|
||
|
|
valid: $validation.valid,
|
||
|
|
optimization_potential: $analysis.compression_potential
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|