Files
homebrew-tap/scripts/parse_formulas.rb
2025-09-21 23:31:15 +03:00

129 lines
3.5 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'json'
require 'fileutils'
require 'pathname'
require 'date'
# Parser class for extracting metadata from Homebrew formulae
class FormulaParser
FORMULA_DIR = File.expand_path('../Formula', __dir__)
OUTPUT_DIR = File.expand_path('../docs/_data', __dir__)
OUTPUT_FILE = File.join(OUTPUT_DIR, 'formulae.json')
# Regex patterns for safe extraction without code evaluation
PATTERNS = {
class_name: /^class\s+(\w+)\s+<\s+Formula/,
desc: /^\s*desc\s+["']([^"']+)["']/,
homepage: /^\s*homepage\s+["']([^"']+)["']/,
url: /^\s*url\s+["']([^"']+)["']/,
version: /^\s*version\s+["']([^"']+)["']/,
sha256: /^\s*sha256\s+["']([a-f0-9]{64})["']/i,
license: /^\s*license\s+["']([^"']+)["']/,
depends_on: /^\s*depends_on\s+["']([^"']+)["']/
}.freeze
def self.run
new.generate_documentation_data
end
def generate_documentation_data
ensure_output_directory
formulae = parse_all_formulae
write_json_output(formulae)
puts "✅ Successfully generated documentation for #{formulae.length} formulae"
end
private
def ensure_output_directory
FileUtils.mkdir_p(OUTPUT_DIR)
end
def parse_all_formulae
formula_files.map { |file| parse_formula(file) }.compact.sort_by { |f| f[:name] }
end
def formula_files
Dir.glob(File.join(FORMULA_DIR, '**', '*.rb'))
end
def parse_formula(file_path)
content = File.read(file_path)
class_name = extract_value(content, :class_name)
return nil unless class_name
formula_name = convert_class_name_to_formula_name(class_name)
return nil if formula_name.nil? || formula_name.empty?
{
name: formula_name,
class_name: class_name,
description: extract_value(content, :desc),
homepage: extract_value(content, :homepage),
url: extract_value(content, :url),
version: extract_version(content),
sha256: extract_value(content, :sha256),
license: extract_value(content, :license),
dependencies: extract_dependencies(content),
file_path: Pathname.new(file_path).relative_path_from(Pathname.new(FORMULA_DIR)).to_s,
last_modified: format_time_iso8601(File.mtime(file_path))
}
rescue StandardError => e
warn "⚠️ Error parsing #{file_path}: #{e.message}"
nil
end
def extract_value(content, pattern_key)
match = content.match(PATTERNS[pattern_key])
match&.[](1)
end
def extract_version(content)
# Try explicit version first, then extract from URL
explicit = extract_value(content, :version)
return explicit if explicit
url = extract_value(content, :url)
return nil unless url
# Common version patterns in URLs
url.match(/v?(\d+(?:\.\d+)+)/)&.[](1)
end
def extract_dependencies(content)
content.scan(PATTERNS[:depends_on]).flatten.uniq
end
def convert_class_name_to_formula_name(class_name)
return nil unless class_name
# Convert CamelCase to kebab-case
class_name
.gsub(/([a-z\d])([A-Z])/, '\1-\2')
.downcase
end
def format_time_iso8601(time)
# Format time manually for compatibility
time.strftime('%Y-%m-%dT%H:%M:%S%z').gsub(/(\d{2})(\d{2})$/, '\1:\2')
end
def write_json_output(formulae)
output = {
tap_name: 'ivuorinen/tap',
generated_at: format_time_iso8601(Time.now),
formulae_count: formulae.length,
formulae: formulae
}
File.write(OUTPUT_FILE, JSON.pretty_generate(output))
end
end
# Run if executed directly
FormulaParser.run if __FILE__ == $PROGRAM_NAME