mirror of
https://github.com/ivuorinen/homebrew-tap.git
synced 2026-02-01 11:44:08 +00:00
334 lines
9.0 KiB
Ruby
334 lines
9.0 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'json'
|
|
require 'fileutils'
|
|
require 'erb'
|
|
require 'pathname'
|
|
require 'time'
|
|
|
|
# Simple static site generator for homebrew tap documentation
|
|
class SiteBuilder
|
|
include ERB::Util
|
|
class PartialContext
|
|
include ERB::Util
|
|
|
|
def initialize(builder, locals)
|
|
@builder = builder
|
|
locals.each do |key, value|
|
|
define_singleton_method(key) { value }
|
|
end
|
|
end
|
|
|
|
def render_partial(name, locals = {})
|
|
@builder.render_partial(name, locals)
|
|
end
|
|
|
|
def format_relative_time(timestamp)
|
|
@builder.format_relative_time(timestamp)
|
|
end
|
|
|
|
def format_date(timestamp)
|
|
@builder.format_date(timestamp)
|
|
end
|
|
|
|
def get_binding
|
|
binding
|
|
end
|
|
end
|
|
DOCS_DIR = File.expand_path('../docs', __dir__)
|
|
DATA_DIR = File.join(DOCS_DIR, '_data')
|
|
OUTPUT_DIR = DOCS_DIR
|
|
THEME_SOURCE_DIR = File.expand_path('../theme', __dir__)
|
|
TEMPLATES_DIR = THEME_SOURCE_DIR
|
|
|
|
def self.build
|
|
new.generate_site
|
|
end
|
|
|
|
def generate_site
|
|
puts "🏗️ Building static site..."
|
|
|
|
setup_directories
|
|
load_data
|
|
generate_assets
|
|
generate_pages
|
|
|
|
puts "✅ Site built successfully in #{OUTPUT_DIR}"
|
|
puts "🌐 Open #{File.join(OUTPUT_DIR, 'index.html')} in your browser"
|
|
end
|
|
|
|
def render_partial(name, locals = {})
|
|
partial_path = File.join(TEMPLATES_DIR, "_#{name}.html.erb")
|
|
unless File.exist?(partial_path)
|
|
raise ArgumentError, "Partial not found: #{partial_path}"
|
|
end
|
|
|
|
context = PartialContext.new(self, locals)
|
|
ERB.new(File.read(partial_path)).result(context.get_binding)
|
|
end
|
|
|
|
def format_relative_time(timestamp)
|
|
return '' unless timestamp
|
|
|
|
begin
|
|
time = Time.parse(timestamp)
|
|
now = Time.now
|
|
diff = now - time
|
|
|
|
case diff
|
|
when 0..59
|
|
'just now'
|
|
when 60..3599
|
|
mins = (diff / 60).to_i
|
|
"#{mins} minute#{mins == 1 ? '' : 's'} ago"
|
|
when 3600..86399
|
|
hours = (diff / 3600).to_i
|
|
"#{hours} hour#{hours == 1 ? '' : 's'} ago"
|
|
when 86400..604799
|
|
days = (diff / 86400).to_i
|
|
"#{days} day#{days == 1 ? '' : 's'} ago"
|
|
when 604800..2419199
|
|
weeks = (diff / 604800).to_i
|
|
"#{weeks} week#{weeks == 1 ? '' : 's'} ago"
|
|
when 2419200..31535999
|
|
months = (diff / 2419200).to_i
|
|
"#{months} month#{months == 1 ? '' : 's'} ago"
|
|
else
|
|
years = (diff / 31536000).to_i
|
|
"#{years} year#{years == 1 ? '' : 's'} ago"
|
|
end
|
|
rescue
|
|
''
|
|
end
|
|
end
|
|
|
|
def format_date(timestamp)
|
|
return '' unless timestamp
|
|
|
|
begin
|
|
Time.parse(timestamp).strftime('%b %d, %Y')
|
|
rescue
|
|
''
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def setup_directories
|
|
FileUtils.mkdir_p(File.join(OUTPUT_DIR, 'formula'))
|
|
unless templates_exist?
|
|
puts "⚠️ Templates not found in #{TEMPLATES_DIR}. Please ensure theme/*.html.erb files exist."
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
def load_data
|
|
formulae_file = File.join(DATA_DIR, 'formulae.json')
|
|
@data = File.exist?(formulae_file) ? JSON.parse(File.read(formulae_file)) : default_data
|
|
end
|
|
|
|
def generate_assets
|
|
copy_assets
|
|
generate_css
|
|
minify_js
|
|
end
|
|
|
|
def generate_pages
|
|
generate_index_page
|
|
generate_formulae_pages
|
|
end
|
|
|
|
def copy_assets
|
|
assets_source_dir = File.join(THEME_SOURCE_DIR, 'assets')
|
|
assets_output_dir = File.join(OUTPUT_DIR, 'assets')
|
|
|
|
# Create the output assets directory if it doesn't exist
|
|
FileUtils.mkdir_p(assets_output_dir)
|
|
|
|
# Check if source assets directory exists
|
|
if Dir.exist?(assets_source_dir)
|
|
# Copy all files recursively, preserving directory structure
|
|
Dir.glob(File.join(assets_source_dir, '**', '*')).each do |source_file|
|
|
next if File.directory?(source_file)
|
|
|
|
# Calculate relative path from source assets dir
|
|
relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(assets_source_dir))
|
|
output_file = File.join(assets_output_dir, relative_path)
|
|
|
|
# Create parent directories if needed
|
|
FileUtils.mkdir_p(File.dirname(output_file))
|
|
|
|
# Copy the file
|
|
FileUtils.cp(source_file, output_file)
|
|
end
|
|
|
|
asset_count = Dir.glob(File.join(assets_source_dir, '**', '*')).reject { |f| File.directory?(f) }.size
|
|
puts "📁 Copied #{asset_count} asset files to #{assets_output_dir}"
|
|
else
|
|
puts "⚠️ Assets source directory not found: #{assets_source_dir}"
|
|
end
|
|
end
|
|
|
|
def generate_css
|
|
css_source_path = File.join(THEME_SOURCE_DIR, 'style.css')
|
|
css_output_path = File.join(OUTPUT_DIR, 'style.css')
|
|
|
|
if File.exist?(css_source_path)
|
|
css_content = File.read(css_source_path)
|
|
minified_css = minify_css(css_content)
|
|
File.write(css_output_path, minified_css)
|
|
puts "📄 Generated and minified CSS (#{minified_css.length} bytes)"
|
|
else
|
|
puts "⚠️ CSS source file not found: #{css_source_path}"
|
|
end
|
|
end
|
|
|
|
|
|
|
|
def minify_css(css)
|
|
css
|
|
.gsub(/\/\*.*?\*\//m, '') # Remove comments
|
|
.gsub(/\s+/, ' ') # Collapse whitespace
|
|
.gsub(/\s*{\s*/, '{') # Remove spaces around braces
|
|
.gsub(/\s*}\s*/, '}')
|
|
.gsub(/\s*:\s*/, ':') # Remove spaces around colons
|
|
.gsub(/\s*;\s*/, ';') # Remove spaces around semicolons
|
|
.gsub(/\s*,\s*/, ',') # Remove spaces around commas
|
|
.strip
|
|
end
|
|
|
|
def minify_js
|
|
js_source_path = File.join(THEME_SOURCE_DIR, 'main.js')
|
|
js_output_path = File.join(OUTPUT_DIR, 'main.js')
|
|
|
|
if File.exist?(js_source_path)
|
|
js_content = File.read(js_source_path)
|
|
minified_js = minify_js_content(js_content)
|
|
File.write(js_output_path, minified_js)
|
|
puts "📄 Generated and minified JavaScript (#{minified_js.length} bytes)"
|
|
else
|
|
puts "⚠️ JavaScript source file not found: #{js_source_path}"
|
|
end
|
|
end
|
|
|
|
|
|
|
|
def minify_js_content(js)
|
|
# Simple minification that preserves string literals
|
|
# This is a basic approach that handles most cases
|
|
result = []
|
|
in_string = false
|
|
in_template = false
|
|
string_char = nil
|
|
i = 0
|
|
|
|
while i < js.length
|
|
char = js[i]
|
|
prev_char = i > 0 ? js[i - 1] : nil
|
|
|
|
# Handle string and template literal boundaries
|
|
if !in_string && !in_template && (char == '"' || char == "'" || char == '`')
|
|
in_string = true if char != '`'
|
|
in_template = true if char == '`'
|
|
string_char = char
|
|
result << char
|
|
elsif (in_string || in_template) && char == string_char && prev_char != '\\'
|
|
in_string = false
|
|
in_template = false
|
|
string_char = nil
|
|
result << char
|
|
elsif in_string || in_template
|
|
# Preserve everything inside strings and template literals
|
|
result << char
|
|
else
|
|
# Outside strings, apply minification
|
|
if char == '/' && i + 1 < js.length
|
|
next_char = js[i + 1]
|
|
if next_char == '/'
|
|
# Skip single-line comment
|
|
i += 1 while i < js.length && js[i] != "\n"
|
|
next
|
|
elsif next_char == '*'
|
|
# Skip multi-line comment
|
|
i += 2
|
|
while i < js.length - 1
|
|
break if js[i] == '*' && js[i + 1] == '/'
|
|
i += 1
|
|
end
|
|
i += 1 # Skip the closing /
|
|
next
|
|
else
|
|
result << char
|
|
end
|
|
elsif char =~ /\s/
|
|
# Only add space if needed between identifiers
|
|
if result.last && result.last =~ /[a-zA-Z0-9_$]/ &&
|
|
i + 1 < js.length && js[i + 1] =~ /[a-zA-Z0-9_$]/
|
|
result << ' '
|
|
end
|
|
else
|
|
result << char
|
|
end
|
|
end
|
|
|
|
i += 1
|
|
end
|
|
|
|
result.join.strip
|
|
end
|
|
|
|
def generate_index_page
|
|
template = load_template('index.html.erb')
|
|
content = template.result(binding)
|
|
File.write(File.join(OUTPUT_DIR, 'index.html'), content)
|
|
end
|
|
|
|
def generate_formulae_pages
|
|
@data['formulae'].each do |formula|
|
|
generate_formula_page(formula)
|
|
end
|
|
|
|
# Generate formulae index
|
|
template = load_template('formulae.html.erb')
|
|
content = template.result(binding)
|
|
File.write(File.join(OUTPUT_DIR, 'formulae.html'), content)
|
|
end
|
|
|
|
def generate_formula_page(formula)
|
|
@formula = formula
|
|
template = load_template('formula.html.erb')
|
|
content = template.result(binding)
|
|
|
|
formula_dir = File.join(OUTPUT_DIR, 'formula')
|
|
FileUtils.mkdir_p(formula_dir)
|
|
File.write(File.join(formula_dir, "#{formula['name']}.html"), content)
|
|
end
|
|
|
|
def load_template(name)
|
|
template_path = File.join(TEMPLATES_DIR, name)
|
|
template_content = File.read(template_path)
|
|
ERB.new(template_content)
|
|
end
|
|
|
|
def templates_exist?
|
|
%w[index.html.erb formulae.html.erb formula.html.erb].all? do |template|
|
|
File.exist?(File.join(TEMPLATES_DIR, template))
|
|
end
|
|
end
|
|
|
|
def default_data
|
|
{
|
|
'tap_name' => 'ivuorinen/homebrew-tap',
|
|
'generated_at' => Time.now.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
|
'formulae_count' => 0,
|
|
'formulae' => []
|
|
}
|
|
end
|
|
end
|
|
|
|
# Allow running this script directly
|
|
if __FILE__ == $PROGRAM_NAME
|
|
SiteBuilder.build
|
|
end
|