mirror of
https://github.com/ivuorinen/homebrew-tap.git
synced 2026-02-07 06:47:10 +00:00
feat: full site
This commit is contained in:
333
scripts/build_site.rb
Normal file
333
scripts/build_site.rb
Normal file
@@ -0,0 +1,333 @@
|
||||
#!/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
|
||||
121
scripts/make.rb
Normal file
121
scripts/make.rb
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
# Simple make-style command runner for homebrew tap
|
||||
class Make
|
||||
COMMANDS = {
|
||||
'build' => 'Build the static site',
|
||||
'serve' => 'Start development server',
|
||||
'parse' => 'Parse formulae and generate JSON data',
|
||||
'clean' => 'Clean generated files',
|
||||
'help' => 'Show this help message'
|
||||
}.freeze
|
||||
|
||||
def self.run(command = nil)
|
||||
new.execute(command || ARGV[0])
|
||||
end
|
||||
|
||||
def execute(command)
|
||||
case command&.downcase
|
||||
when 'build'
|
||||
build
|
||||
when 'serve'
|
||||
serve
|
||||
when 'parse'
|
||||
parse
|
||||
when 'clean'
|
||||
clean
|
||||
when 'help', nil
|
||||
help
|
||||
else
|
||||
puts "❌ Unknown command: #{command}"
|
||||
help
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build
|
||||
puts "🏗️ Building homebrew tap documentation..."
|
||||
|
||||
success = system('ruby', script_path('parse_formulas.rb'))
|
||||
exit 1 unless success
|
||||
|
||||
success = system('ruby', script_path('build_site.rb'))
|
||||
exit 1 unless success
|
||||
|
||||
puts "✅ Build complete!"
|
||||
end
|
||||
|
||||
def serve
|
||||
port = ARGV[1]&.to_i || 4000
|
||||
host = ARGV[2] || 'localhost'
|
||||
|
||||
puts "🚀 Starting development server on http://#{host}:#{port}"
|
||||
|
||||
exec('ruby', script_path('serve.rb'), port.to_s, host)
|
||||
end
|
||||
|
||||
def parse
|
||||
puts "📋 Parsing formulae..."
|
||||
|
||||
success = system('ruby', script_path('parse_formulas.rb'))
|
||||
exit 1 unless success
|
||||
|
||||
puts "✅ Formulae parsing complete!"
|
||||
end
|
||||
|
||||
def clean
|
||||
puts "🧹 Cleaning generated files..."
|
||||
|
||||
files_to_clean = [
|
||||
docs_path('index.html'),
|
||||
docs_path('formulae.html'),
|
||||
docs_path('formula'),
|
||||
docs_path('_templates'),
|
||||
docs_path('_data', 'formulae.json'),
|
||||
docs_path('style.css'),
|
||||
docs_path('main.js')
|
||||
]
|
||||
|
||||
files_to_clean.each do |path|
|
||||
if File.exist?(path)
|
||||
FileUtils.rm_rf(path)
|
||||
puts " 🗑️ Removed #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
puts "✅ Clean complete!"
|
||||
end
|
||||
|
||||
def help
|
||||
puts "Homebrew Tap Documentation Builder"
|
||||
puts
|
||||
puts "Usage: ruby scripts/make.rb <command>"
|
||||
puts
|
||||
puts "Commands:"
|
||||
COMMANDS.each do |cmd, desc|
|
||||
puts " #{cmd.ljust(10)} #{desc}"
|
||||
end
|
||||
puts
|
||||
puts "Examples:"
|
||||
puts " ruby scripts/make.rb build # Build the site"
|
||||
puts " ruby scripts/make.rb serve # Start server on port 4000"
|
||||
puts " ruby scripts/make.rb serve 3000 # Start server on port 3000"
|
||||
puts " ruby scripts/make.rb serve 3000 0.0.0.0 # Start server on all interfaces"
|
||||
end
|
||||
|
||||
def script_path(filename)
|
||||
File.join(__dir__, filename)
|
||||
end
|
||||
|
||||
def docs_path(*parts)
|
||||
File.join(__dir__, '..', 'docs', *parts)
|
||||
end
|
||||
end
|
||||
|
||||
# Run if executed directly
|
||||
Make.run if __FILE__ == $PROGRAM_NAME
|
||||
@@ -114,7 +114,7 @@ class FormulaParser
|
||||
|
||||
def write_json_output(formulae)
|
||||
output = {
|
||||
tap_name: 'ivuorinen/homebrew-tap',
|
||||
tap_name: 'ivuorinen/tap',
|
||||
generated_at: format_time_iso8601(Time.now),
|
||||
formulae_count: formulae.length,
|
||||
formulae: formulae
|
||||
|
||||
170
scripts/serve.rb
Normal file
170
scripts/serve.rb
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'fileutils'
|
||||
require_relative 'parse_formulas'
|
||||
require_relative 'build_site'
|
||||
|
||||
# Simple development server for the homebrew tap documentation
|
||||
class DevServer
|
||||
def self.serve(port: 4000, host: 'localhost')
|
||||
new(port: port, host: host).start
|
||||
end
|
||||
|
||||
def initialize(port: 4000, host: 'localhost')
|
||||
@port = port
|
||||
@host = host
|
||||
@site_dir = File.expand_path('../docs', __dir__)
|
||||
@docs_dir = File.expand_path('../docs', __dir__)
|
||||
end
|
||||
|
||||
def start
|
||||
puts "🔄 Building site..."
|
||||
build_site
|
||||
|
||||
puts "🚀 Starting development server..."
|
||||
puts "📍 Server address: http://#{@host}:#{@port}"
|
||||
puts "📁 Serving from: #{@site_dir}"
|
||||
puts "💡 Press Ctrl+C to stop"
|
||||
|
||||
start_server
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_site
|
||||
# Generate formulae data
|
||||
FormulaParser.run
|
||||
|
||||
# Build static site
|
||||
SiteBuilder.build
|
||||
end
|
||||
|
||||
def start_server
|
||||
server = WEBrick::HTTPServer.new(
|
||||
Port: @port,
|
||||
Host: @host,
|
||||
DocumentRoot: @site_dir,
|
||||
Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
|
||||
AccessLog: [[
|
||||
$stderr,
|
||||
WEBrick::AccessLog::COMBINED_LOG_FORMAT
|
||||
]]
|
||||
)
|
||||
|
||||
# Handle Ctrl+C gracefully
|
||||
trap('INT') do
|
||||
puts "\n👋 Stopping server..."
|
||||
server.shutdown
|
||||
end
|
||||
|
||||
# Add custom mime types if needed
|
||||
server.config[:MimeTypes]['json'] = 'application/json'
|
||||
|
||||
# Add auto-rebuild on file changes (simple polling)
|
||||
start_file_watcher
|
||||
|
||||
server.start
|
||||
end
|
||||
|
||||
def start_file_watcher
|
||||
Thread.new do
|
||||
last_mtime = get_max_mtime
|
||||
rebuild_pending = false
|
||||
watched_files = get_watched_files_count
|
||||
|
||||
puts "👀 Watching #{watched_files} files for changes..."
|
||||
|
||||
loop do
|
||||
sleep 1
|
||||
current_mtime = get_max_mtime
|
||||
|
||||
if current_mtime > last_mtime && !rebuild_pending
|
||||
rebuild_pending = true
|
||||
changed_file = find_changed_file(last_mtime)
|
||||
puts "📝 Changed: #{changed_file}" if changed_file
|
||||
puts "🔄 Rebuilding in 1 second..."
|
||||
|
||||
# Debounce: wait for additional changes
|
||||
sleep 1
|
||||
|
||||
# Check if more changes occurred during debounce period
|
||||
final_mtime = get_max_mtime
|
||||
|
||||
puts "🔨 Building site..."
|
||||
build_site
|
||||
last_mtime = final_mtime
|
||||
rebuild_pending = false
|
||||
puts "✅ Rebuild complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_watched_files_count
|
||||
files = get_all_watched_files
|
||||
files.select { |f| File.exist?(f) && !File.directory?(f) }.count
|
||||
end
|
||||
|
||||
def find_changed_file(since_time)
|
||||
files = get_all_watched_files
|
||||
files.select { |f| File.exist?(f) && !File.directory?(f) }
|
||||
.find { |f| File.mtime(f) > since_time }
|
||||
&.sub(File.expand_path('..', __dir__) + '/', '')
|
||||
end
|
||||
|
||||
def get_all_watched_files
|
||||
[
|
||||
# Watch Formula files for changes
|
||||
Dir.glob(File.expand_path('../Formula/**/*.rb', __dir__)),
|
||||
# Watch all theme files including partials
|
||||
Dir.glob(File.expand_path('../theme/**/*', __dir__)),
|
||||
# Specifically watch for erb templates and partials
|
||||
Dir.glob(File.expand_path('../theme/*.erb', __dir__)),
|
||||
Dir.glob(File.expand_path('../theme/_*.erb', __dir__)),
|
||||
Dir.glob(File.expand_path('../theme/*.html.erb', __dir__)),
|
||||
Dir.glob(File.expand_path('../theme/_*.html.erb', __dir__)),
|
||||
# Watch CSS and JS
|
||||
Dir.glob(File.expand_path('../theme/*.css', __dir__)),
|
||||
Dir.glob(File.expand_path('../theme/*.js', __dir__)),
|
||||
# Watch assets directory
|
||||
Dir.glob(File.expand_path('../theme/assets/**/*', __dir__)),
|
||||
# Watch build scripts for changes
|
||||
[File.expand_path('../scripts/parse_formulas.rb', __dir__)],
|
||||
[File.expand_path('../scripts/build_site.rb', __dir__)],
|
||||
# Watch Makefile
|
||||
[File.expand_path('../Makefile', __dir__)]
|
||||
].flatten.compact.uniq
|
||||
end
|
||||
|
||||
def get_max_mtime
|
||||
files_to_watch = get_all_watched_files
|
||||
|
||||
# Filter out non-existent files and directories, get modification times
|
||||
existing_files = files_to_watch.select { |f| File.exist?(f) && !File.directory?(f) }
|
||||
|
||||
if existing_files.empty?
|
||||
Time.at(0)
|
||||
else
|
||||
existing_files.map { |f| File.mtime(f) }.max
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Command line interface
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
# Check for --list-watched flag
|
||||
if ARGV.include?('--list-watched')
|
||||
server = DevServer.new
|
||||
files = server.send(:get_all_watched_files).select { |f| File.exist?(f) && !File.directory?(f) }
|
||||
puts "📋 Watching #{files.count} files:"
|
||||
files.sort.each { |f| puts " - #{f.sub(File.expand_path('..', __dir__) + '/', '')}" }
|
||||
exit 0
|
||||
end
|
||||
|
||||
port = ARGV[0]&.to_i || 4000
|
||||
host = ARGV[1] || 'localhost'
|
||||
|
||||
DevServer.serve(port: port, host: host)
|
||||
end
|
||||
Reference in New Issue
Block a user