diff --git a/scripts/array_extensions.rb b/scripts/array_extensions.rb new file mode 100644 index 0000000..552a252 --- /dev/null +++ b/scripts/array_extensions.rb @@ -0,0 +1,9 @@ +# typed: strict +# frozen_string_literal: true + +# Simple polyfill for Homebrew extensions +class Array + def exclude?(item) + !include?(item) + end +end diff --git a/scripts/asset_processor.rb b/scripts/asset_processor.rb new file mode 100644 index 0000000..465efdb --- /dev/null +++ b/scripts/asset_processor.rb @@ -0,0 +1,77 @@ +# typed: strict +# frozen_string_literal: true + +require "fileutils" +require "pathname" +require "terser" +require "cssminify2" + +# Module for processing and copying assets +module AssetProcessor + DOCS_DIR = File.expand_path("../docs", __dir__).freeze + OUTPUT_DIR = DOCS_DIR + THEME_SOURCE_DIR = File.expand_path("../theme", __dir__).freeze + + def copy_assets + copy_asset_files + end + + def generate_css + css_path = File.join(THEME_SOURCE_DIR, "style.css") + output_path = File.join(OUTPUT_DIR, "style.css") + + return unless File.exist?(css_path) + + css_content = File.read(css_path) + minified_css = CSSminify2.compress(css_content) + File.write(output_path, minified_css) + puts "📄 Generated CSS file: #{output_path}" + end + + def minify_js + js_path = File.join(THEME_SOURCE_DIR, "main.js") + output_path = File.join(OUTPUT_DIR, "main.js") + + return unless File.exist?(js_path) + + js_content = File.read(js_path) + minified_js = Terser.new.compile(js_content) + File.write(output_path, minified_js) + puts "🔧 Generated JS file: #{output_path}" + end + + private + + def copy_asset_files + assets_source_dir = File.join(THEME_SOURCE_DIR, "assets") + assets_output_dir = File.join(OUTPUT_DIR, "assets") + + FileUtils.mkdir_p(assets_output_dir) + + return handle_missing_assets(assets_source_dir) unless Dir.exist?(assets_source_dir) + + copy_files_recursively(assets_source_dir, assets_output_dir) + end + + def handle_missing_assets(assets_source_dir) + puts "⚠️ Assets source directory not found: #{assets_source_dir}" + end + + def copy_files_recursively(source_dir, output_dir) + asset_files = Dir.glob(File.join(source_dir, "**", "*")).reject { |f| File.directory?(f) } + + asset_files.each do |source_file| + copy_single_asset(source_file, source_dir, output_dir) + end + + puts "📁 Copied #{asset_files.count} asset files to #{output_dir}" + end + + def copy_single_asset(source_file, source_dir, output_dir) + relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir)) + output_file = File.join(output_dir, relative_path) + + FileUtils.mkdir_p(File.dirname(output_file)) + FileUtils.cp(source_file, output_file) + end +end diff --git a/scripts/build_site.rb b/scripts/build_site.rb index 3098fb5..2aa8f08 100755 --- a/scripts/build_site.rb +++ b/scripts/build_site.rb @@ -9,146 +9,9 @@ require "pathname" require "time" require "terser" require "cssminify2" - -# Simple polyfill for Homebrew extensions -class Array - def exclude?(item) - !include?(item) - end -end - -# Simple static site generator for homebrew tap documentation -# Module for formatting timestamps and dates -module TimeFormatter - SECONDS_PER_MINUTE = 60 - SECONDS_PER_HOUR = 3600 - SECONDS_PER_DAY = 86_400 - SECONDS_PER_WEEK = 604_800 - SECONDS_PER_MONTH = 2_419_200 - SECONDS_PER_YEAR = 31_536_000 - - def format_relative_time(timestamp) - return "" unless timestamp - - begin - diff = calculate_time_difference(timestamp) - return "just now" if diff < SECONDS_PER_MINUTE - - format_time_by_category(diff) - rescue - "" - end - end - - def format_date(timestamp) - return "" unless timestamp - - begin - Time.parse(timestamp).strftime("%b %d, %Y") - rescue - "" - end - end - - private - - def calculate_time_difference(timestamp) - time = Time.parse(timestamp) - Time.now - time - end - - def format_time_by_category(diff) - case diff - when SECONDS_PER_MINUTE...SECONDS_PER_HOUR - format_time_unit(diff / SECONDS_PER_MINUTE, "minute") - when SECONDS_PER_HOUR...SECONDS_PER_DAY - format_time_unit(diff / SECONDS_PER_HOUR, "hour") - when SECONDS_PER_DAY...SECONDS_PER_WEEK - format_time_unit(diff / SECONDS_PER_DAY, "day") - when SECONDS_PER_WEEK...SECONDS_PER_MONTH - format_time_unit(diff / SECONDS_PER_WEEK, "week") - when SECONDS_PER_MONTH...SECONDS_PER_YEAR - format_time_unit(diff / SECONDS_PER_MONTH, "month") - else - format_time_unit(diff / SECONDS_PER_YEAR, "year") - end - end - - def format_time_unit(value, unit) - count = value.to_i - "#{count} #{unit}#{"s" if count != 1} ago" - end -end - -# Module for processing and copying assets -module AssetProcessor - DOCS_DIR = File.expand_path("../docs", __dir__).freeze - OUTPUT_DIR = DOCS_DIR - THEME_SOURCE_DIR = File.expand_path("../theme", __dir__).freeze - - def copy_assets - copy_asset_files - end - - def generate_css - css_path = File.join(THEME_SOURCE_DIR, "style.css") - output_path = File.join(OUTPUT_DIR, "style.css") - - return unless File.exist?(css_path) - - css_content = File.read(css_path) - minified_css = CSSminify2.compress(css_content) - File.write(output_path, minified_css) - puts "📄 Generated CSS file: #{output_path}" - end - - def minify_js - js_path = File.join(THEME_SOURCE_DIR, "main.js") - output_path = File.join(OUTPUT_DIR, "main.js") - - return unless File.exist?(js_path) - - js_content = File.read(js_path) - minified_js = Terser.new.compile(js_content) - File.write(output_path, minified_js) - puts "🔧 Generated JS file: #{output_path}" - end - - private - - def copy_asset_files - assets_source_dir = File.join(THEME_SOURCE_DIR, "assets") - assets_output_dir = File.join(OUTPUT_DIR, "assets") - - FileUtils.mkdir_p(assets_output_dir) - - return handle_missing_assets(assets_source_dir) unless Dir.exist?(assets_source_dir) - - copy_files_recursively(assets_source_dir, assets_output_dir) - end - - def handle_missing_assets(assets_source_dir) - puts "⚠️ Assets source directory not found: #{assets_source_dir}" - end - - def copy_files_recursively(source_dir, output_dir) - asset_files = Dir.glob(File.join(source_dir, "**", "*")).reject { |f| File.directory?(f) } - - asset_files.each do |source_file| - copy_single_asset(source_file, source_dir, output_dir) - end - - puts "📁 Copied #{asset_files.count} asset files to #{output_dir}" - end - - def copy_single_asset(source_file, source_dir, output_dir) - relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir)) - output_file = File.join(output_dir, relative_path) - - FileUtils.mkdir_p(File.dirname(output_file)) - FileUtils.cp(source_file, output_file) - end -end +require_relative "array_extensions" +require_relative "time_formatter" +require_relative "asset_processor" # Static site generator for homebrew tap documentation class SiteBuilder diff --git a/scripts/file_watcher.rb b/scripts/file_watcher.rb new file mode 100644 index 0000000..e8d1051 --- /dev/null +++ b/scripts/file_watcher.rb @@ -0,0 +1,123 @@ +# typed: strict +# frozen_string_literal: true + +# Module for handling file watching and change detection. +# Classes including this module must define a `build_site` method. +module FileWatcher + def start_file_watcher + Thread.new do + last_mtime = max_mtime + rebuild_pending = false + watched_files = watched_files_count + + puts "👀 Watching #{watched_files} files for changes..." + + loop do + sleep 1 + current_mtime = max_mtime + + next if should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending) + + rebuild_pending = true + handle_file_change(last_mtime) + last_mtime = perform_rebuild_with_debounce + rebuild_pending = false + puts "✅ Rebuild complete" + rescue => e + puts "⚠️ File watcher error: #{e.message}" + puts "📍 Backtrace: #{e.backtrace.first(3).join(", ")}" + rebuild_pending = false + sleep 2 + puts "🔄 File watcher continuing..." + end + end + end + + def watched_files_count + all_watched_files.count { |file| !File.directory?(file) } + end + + def find_changed_file(since_mtime) + all_watched_files.find do |file| + File.exist?(file) && !File.directory?(file) && File.mtime(file) > since_mtime + end + end + + def all_watched_files + [ + formula_files, + theme_files, + template_files, + style_and_script_files, + asset_files, + build_script_files, + config_files, + ].flatten.compact.uniq + end + + def max_mtime + all_watched_files + .select { |file| File.exist?(file) && !File.directory?(file) } + .map { |file| File.mtime(file) } + .max || Time.at(0) + end + + private + + def should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending) + current_mtime <= last_mtime || rebuild_pending + end + + def handle_file_change(last_mtime) + changed_file = find_changed_file(last_mtime) + puts "📝 Changed: #{changed_file}" if changed_file + puts "🔄 Rebuilding in 1 second..." + end + + def perform_rebuild_with_debounce + sleep 1 # Debounce: wait for additional changes + final_mtime = max_mtime + puts "🔨 Building site..." + build_site + final_mtime + end + + def formula_files + Dir.glob(File.expand_path("../Formula/**/*.rb", __dir__)) + end + + def theme_files + Dir.glob(File.expand_path("../theme/**/*", __dir__)) + end + + def template_files + [ + 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__)), + ].flatten + end + + def style_and_script_files + [ + Dir.glob(File.expand_path("../theme/*.css", __dir__)), + Dir.glob(File.expand_path("../theme/*.js", __dir__)), + ].flatten + end + + def asset_files + Dir.glob(File.expand_path("../theme/assets/**/*", __dir__)) + end + + def build_script_files + [ + File.expand_path("../scripts/parse_formulas.rb", __dir__), + File.expand_path("../scripts/build_site.rb", __dir__), + ] + end + + def config_files + [File.expand_path("../Makefile", __dir__)] + end +end diff --git a/scripts/parse_formulas.rb b/scripts/parse_formulas.rb index 5d5ae5c..7c64ca2 100755 --- a/scripts/parse_formulas.rb +++ b/scripts/parse_formulas.rb @@ -6,14 +6,7 @@ require "json" require "fileutils" require "pathname" require "date" - -# Simple polyfill for Homebrew extensions -class String - def blank? - # Polyfill implementation to avoid external dependencies - nil? || empty? # rubocop:disable Homebrew/Blank, Lint/RedundantCopDisableDirective - end -end +require_relative "string_extensions" # Parser class for extracting metadata from Homebrew formulae class FormulaParser diff --git a/scripts/serve.rb b/scripts/serve.rb index c2b29e0..c2ad75e 100755 --- a/scripts/serve.rb +++ b/scripts/serve.rb @@ -6,127 +6,7 @@ require "webrick" require "fileutils" require_relative "parse_formulas" require_relative "build_site" - -# Simple development server for the homebrew tap documentation -# Module for handling file watching and change detection -module FileWatcher - def start_file_watcher - Thread.new do - last_mtime = max_mtime - rebuild_pending = false - watched_files = watched_files_count - - puts "👀 Watching #{watched_files} files for changes..." - - loop do - sleep 1 - current_mtime = max_mtime - - next if should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending) - - rebuild_pending = true - handle_file_change(last_mtime) - last_mtime = perform_rebuild_with_debounce - rebuild_pending = false - puts "✅ Rebuild complete" - rescue => e - puts "⚠️ File watcher error: #{e.message}" - puts "📍 Backtrace: #{e.backtrace.first(3).join(", ")}" - rebuild_pending = false - sleep 2 - puts "🔄 File watcher continuing..." - end - end - end - - def watched_files_count - all_watched_files.count { |file| !File.directory?(file) } - end - - def find_changed_file(since_mtime) - all_watched_files.find do |file| - File.exist?(file) && !File.directory?(file) && File.mtime(file) > since_mtime - end - end - - def all_watched_files - [ - formula_files, - theme_files, - template_files, - style_and_script_files, - asset_files, - build_script_files, - config_files, - ].flatten.compact.uniq - end - - def max_mtime - all_watched_files - .select { |file| File.exist?(file) && !File.directory?(file) } - .map { |file| File.mtime(file) } - .max || Time.at(0) - end - - private - - def should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending) - current_mtime <= last_mtime || rebuild_pending - end - - def handle_file_change(last_mtime) - changed_file = find_changed_file(last_mtime) - puts "📝 Changed: #{changed_file}" if changed_file - puts "🔄 Rebuilding in 1 second..." - end - - def perform_rebuild_with_debounce - sleep 1 # Debounce: wait for additional changes - final_mtime = max_mtime - puts "🔨 Building site..." - build_site - final_mtime - end - - def formula_files - Dir.glob(File.expand_path("../Formula/**/*.rb", __dir__)) - end - - def theme_files - Dir.glob(File.expand_path("../theme/**/*", __dir__)) - end - - def template_files - [ - 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__)), - ].flatten - end - - def style_and_script_files - [ - Dir.glob(File.expand_path("../theme/*.css", __dir__)), - Dir.glob(File.expand_path("../theme/*.js", __dir__)), - ].flatten - end - - def asset_files - Dir.glob(File.expand_path("../theme/assets/**/*", __dir__)) - end - - def build_script_files - [ - File.expand_path("../scripts/parse_formulas.rb", __dir__), - File.expand_path("../scripts/build_site.rb", __dir__), - ] - end - - def config_files - [File.expand_path("../Makefile", __dir__)] - end -end +require_relative "file_watcher" # Development server with file watching for homebrew tap documentation class DevServer diff --git a/scripts/string_extensions.rb b/scripts/string_extensions.rb new file mode 100644 index 0000000..f4c7934 --- /dev/null +++ b/scripts/string_extensions.rb @@ -0,0 +1,10 @@ +# typed: strict +# frozen_string_literal: true + +# Simple polyfill for Homebrew extensions +class String + def blank? + # Polyfill implementation to avoid external dependencies + nil? || empty? # rubocop:disable Homebrew/Blank, Lint/RedundantCopDisableDirective + end +end diff --git a/scripts/time_formatter.rb b/scripts/time_formatter.rb new file mode 100644 index 0000000..6012994 --- /dev/null +++ b/scripts/time_formatter.rb @@ -0,0 +1,66 @@ +# typed: strict +# frozen_string_literal: true + +require "time" + +# Module for formatting timestamps and dates +module TimeFormatter + SECONDS_PER_MINUTE = 60 + SECONDS_PER_HOUR = 3600 + SECONDS_PER_DAY = 86_400 + SECONDS_PER_WEEK = 604_800 + SECONDS_PER_MONTH = 2_419_200 + SECONDS_PER_YEAR = 31_536_000 + + def format_relative_time(timestamp) + return "" unless timestamp + + begin + diff = calculate_time_difference(timestamp) + return "just now" if diff < SECONDS_PER_MINUTE + + format_time_by_category(diff) + rescue + "" + end + end + + def format_date(timestamp) + return "" unless timestamp + + begin + Time.parse(timestamp).strftime("%b %d, %Y") + rescue + "" + end + end + + private + + def calculate_time_difference(timestamp) + time = Time.parse(timestamp) + Time.now - time + end + + def format_time_by_category(diff) + case diff + when SECONDS_PER_MINUTE...SECONDS_PER_HOUR + format_time_unit(diff / SECONDS_PER_MINUTE, "minute") + when SECONDS_PER_HOUR...SECONDS_PER_DAY + format_time_unit(diff / SECONDS_PER_HOUR, "hour") + when SECONDS_PER_DAY...SECONDS_PER_WEEK + format_time_unit(diff / SECONDS_PER_DAY, "day") + when SECONDS_PER_WEEK...SECONDS_PER_MONTH + format_time_unit(diff / SECONDS_PER_WEEK, "week") + when SECONDS_PER_MONTH...SECONDS_PER_YEAR + format_time_unit(diff / SECONDS_PER_MONTH, "month") + else + format_time_unit(diff / SECONDS_PER_YEAR, "year") + end + end + + def format_time_unit(value, unit) + count = value.to_i + "#{count} #{unit}#{"s" if count != 1} ago" + end +end