diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4aa446a..0c60fd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,4 @@ +--- name: CI on: push: @@ -5,23 +6,25 @@ on: pull_request: branches: [main] -permissions: - contents: read - pull-requests: write - actions: read - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: read-all + jobs: test-bot: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-13, macos-14] + os: [ubuntu-22.04, macos-14] runs-on: ${{ matrix.os }} + permissions: + contents: read + pull-requests: write + actions: read + steps: - name: Set up Homebrew id: set-up-homebrew @@ -55,4 +58,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: bottles_${{ matrix.os }} - path: '*.bottle.*' + path: "*.bottle.*" diff --git a/.github/workflows/pages-build.yml b/.github/workflows/pages-build.yml index f4a7eef..bdb8a7b 100644 --- a/.github/workflows/pages-build.yml +++ b/.github/workflows/pages-build.yml @@ -1,17 +1,16 @@ +--- name: Build and Deploy Documentation on: push: branches: [main] paths: - - 'Formula/**' - - 'docs/**' - - 'scripts/**' + - "Formula/**" + - "docs/**" + - "scripts/**" + - "theme/**" workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write +permissions: read-all concurrency: group: pages @@ -20,6 +19,11 @@ concurrency: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + steps: - name: Checkout uses: actions/checkout@v5 @@ -28,30 +32,21 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.4' - bundler-cache: true - - name: Parse Formulae and Generate Data + - name: Parse Formulae and Build Site run: | ruby scripts/parse_formulas.rb echo "Generated formulae.json with $(jq '.formulae | length' docs/_data/formulae.json) formulae" + ruby scripts/build_site.rb - name: Setup Pages id: pages uses: actions/configure-pages@v5 - - name: Build Jekyll Site - run: | - cd docs - bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" - env: - JEKYLL_ENV: production - - name: Upload Pages Artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/_site + path: docs deploy: environment: @@ -59,6 +54,12 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build + + permissions: + contents: read + pages: write + id-token: write + steps: - name: Deploy to GitHub Pages id: deployment diff --git a/.gitignore b/.gitignore index 7ae97b7..79aae00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,13 @@ Gemfile.lock .bundle/ vendor/bundle/ -# Jekyll -docs/_site/ -docs/.sass-cache/ -docs/.jekyll-cache/ -docs/.jekyll-metadata +# Generated site files +docs/**/*.html +docs/*.css +docs/*.js +docs/_data/formulae.json +docs/assets/* +!docs/assets/.gitkeep # macOS .DS_Store @@ -23,5 +25,5 @@ docs/.jekyll-metadata # Logs *.log -# Generated files -docs/_data/formulae.json +# Other +AGENTS.md diff --git a/.ruby-version b/.ruby-version index 4f5e697..9c25013 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.5 +3.3.6 diff --git a/Formula/e/example-tool.rb b/Formula/e/example-tool.rbx similarity index 100% rename from Formula/e/example-tool.rb rename to Formula/e/example-tool.rbx diff --git a/Formula/e/example-tool2.rbx b/Formula/e/example-tool2.rbx new file mode 100644 index 0000000..dfd6e64 --- /dev/null +++ b/Formula/e/example-tool2.rbx @@ -0,0 +1,17 @@ +class ExampleTool2 < Formula + desc "A second example tool to demonstrate the tap functionality" + homepage "https://github.com/ivuorinen/example-tool2" + url "https://github.com/ivuorinen/example-tool2/archive/v2.0.0.tar.gz" + sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + license "MIT" + + depends_on "go" => :build + + def install + system "go", "build", *std_go_args(ldflags: "-s -w") + end + + test do + assert_match "example-tool2 version 2.0.0", shell_output("#{bin}/example-tool2 --version") + end +end diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dbb471d --- /dev/null +++ b/Makefile @@ -0,0 +1,176 @@ +# Homebrew Tap Makefile +# Provides convenient commands for building and managing the tap documentation + +.PHONY: help build serve parse clean test install dev setup check + +# Default target +.DEFAULT_GOAL := help + +# Variables +RUBY := ruby +PORT := 4000 +HOST := localhost +SCRIPTS_DIR := scripts +THEME_DIR := theme +DOCS_DIR := docs +FORMULA_DIR := Formula + +help: ## Show this help message + @echo "Homebrew Tap Documentation Builder" + @echo "" + @echo "Usage: make " + @echo "" + @echo "Targets:" + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-12s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "" + @echo "Examples:" + @echo " make build # Build the documentation site" + @echo " make serve # Start development server on port 4000" + @echo " make serve PORT=3000 # Start development server on port 3000" + @echo " make dev # Full development setup (parse + build + serve)" + +build: ## Build the static documentation site + @echo "๐Ÿ—๏ธ Building homebrew tap documentation..." + @$(RUBY) $(SCRIPTS_DIR)/parse_formulas.rb + @$(RUBY) $(SCRIPTS_DIR)/build_site.rb + @echo "โœ… Build complete!" + +serve: ## Start development server (default: localhost:4000) + @echo "๐Ÿš€ Starting development server on http://$(HOST):$(PORT)" + @$(RUBY) $(SCRIPTS_DIR)/serve.rb $(PORT) $(HOST) + +parse: ## Parse formulae and generate JSON data only + @echo "๐Ÿ“‹ Parsing formulae..." + @$(RUBY) $(SCRIPTS_DIR)/parse_formulas.rb + @echo "โœ… Formulae parsing complete!" + +clean: ## Clean all generated files + @echo "๐Ÿงน Cleaning generated files..." + @$(RUBY) $(SCRIPTS_DIR)/make.rb clean + @echo "โœ… Clean complete!" + +dev: parse build serve ## Full development workflow: parse, build, and serve + +setup: ## Initial project setup and dependency check + @echo "๐Ÿ”ง Setting up homebrew tap development environment..." + @which $(RUBY) > /dev/null || (echo "โŒ Ruby not found. Please install Ruby first." && exit 1) + @test -d $(FORMULA_DIR) || (echo "โŒ Formula directory not found" && exit 1) + @test -d $(THEME_DIR) || (echo "โŒ Theme directory not found" && exit 1) + @test -f $(THEME_DIR)/index.html.erb || (echo "โŒ Theme templates not found" && exit 1) + @echo "โœ… Environment setup complete!" + +check: ## Check if all required files and directories exist + @echo "๐Ÿ” Checking project structure..." + @test -d $(SCRIPTS_DIR) && echo "โœ… Scripts directory exists" || echo "โŒ Scripts directory missing" + @test -d $(THEME_DIR) && echo "โœ… Theme directory exists" || echo "โŒ Theme directory missing" + @test -d $(FORMULA_DIR) && echo "โœ… Formula directory exists" || echo "โŒ Formula directory missing" + @test -f $(THEME_DIR)/index.html.erb && echo "โœ… Index template exists" || echo "โŒ Index template missing" + @test -f $(THEME_DIR)/formulae.html.erb && echo "โœ… Formulae template exists" || echo "โŒ Formulae template missing" + @test -f $(THEME_DIR)/formula.html.erb && echo "โœ… Formula template exists" || echo "โŒ Formula template missing" + @test -f $(THEME_DIR)/style.css && echo "โœ… CSS file exists" || echo "โŒ CSS file missing" + @test -f $(THEME_DIR)/main.js && echo "โœ… JavaScript file exists" || echo "โŒ JavaScript file missing" + +test: check ## Run tests and validation + @echo "๐Ÿงช Running validation tests..." + @$(RUBY) -c $(SCRIPTS_DIR)/parse_formulas.rb && echo "โœ… parse_formulas.rb syntax OK" || echo "โŒ parse_formulas.rb syntax error" + @$(RUBY) -c $(SCRIPTS_DIR)/build_site.rb && echo "โœ… build_site.rb syntax OK" || echo "โŒ build_site.rb syntax error" + @$(RUBY) -c $(SCRIPTS_DIR)/serve.rb && echo "โœ… serve.rb syntax OK" || echo "โŒ serve.rb syntax error" + @$(RUBY) -c $(SCRIPTS_DIR)/make.rb && echo "โœ… make.rb syntax OK" || echo "โŒ make.rb syntax error" + @echo "โœ… All tests passed!" + +install: ## Install development dependencies (if Gemfile exists) + @if [ -f Gemfile ]; then \ + echo "๐Ÿ“ฆ Installing Ruby dependencies..."; \ + bundle install; \ + echo "โœ… Dependencies installed!"; \ + else \ + echo "โ„น๏ธ No Gemfile found, skipping dependency installation"; \ + fi + +watch: ## Watch for file changes and auto-rebuild (alias for serve) + @$(MAKE) serve + +# Advanced targets +serve-all: ## Start server accessible from all interfaces (0.0.0.0) + @$(MAKE) serve HOST=0.0.0.0 + +serve-3000: ## Start server on port 3000 + @$(MAKE) serve PORT=3000 + +serve-8080: ## Start server on port 8080 + @$(MAKE) serve PORT=8080 + +build-production: ## Build for production deployment + @echo "๐Ÿญ Building for production..." + @$(MAKE) clean + @$(MAKE) build + @echo "โœ… Production build complete!" + +# Homebrew-specific targets +tap-test: ## Test the tap installation locally + @echo "๐Ÿบ Testing tap installation..." + @brew tap-new ivuorinen/homebrew-tap --no-git 2>/dev/null || true + @brew audit --strict $(FORMULA_DIR)/*.rb || echo "โš ๏ธ Some formulae may have audit issues" + +tap-install: ## Install this tap locally for testing + @echo "๐Ÿบ Installing tap locally..." + @brew tap $$(pwd) + +formula-new: ## Create a new formula template (usage: make formula-new NAME=myformula) + @if [ -z "$(NAME)" ]; then \ + echo "โŒ Please provide a formula name: make formula-new NAME=myformula"; \ + exit 1; \ + fi + @echo "๐Ÿ“ Creating new formula: $(NAME)" + @FIRST_CHAR=$$(echo $(NAME) | cut -c1); \ + CLASS_NAME=$$($(RUBY) -e "puts '$(NAME)'.split('-').map(&:capitalize).join"); \ + mkdir -p $(FORMULA_DIR)/$$FIRST_CHAR; \ + echo "class $$CLASS_NAME < Formula" > $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' desc "Description of $(NAME)"' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' homepage "https://github.com/ivuorinen/$(NAME)"' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' url "https://github.com/ivuorinen/$(NAME)/archive/v1.0.0.tar.gz"' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' sha256 "REPLACE_WITH_ACTUAL_SHA256"' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' license "MIT"' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo '' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' def install' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' # Installation steps here' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' end' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo '' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' test do' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' # Test steps here' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo ' end' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb; \ + echo 'end' >> $(FORMULA_DIR)/$$FIRST_CHAR/$(NAME).rb + @echo "โœ… Formula template created at $(FORMULA_DIR)/$$(echo $(NAME) | cut -c1)/$(NAME).rb" + @echo "๐Ÿ“ Remember to update the URL, SHA256, and implementation!" + +# Information targets +info: ## Show project information + @echo "๐Ÿ“‹ Homebrew Tap Information" + @echo "==========================" + @echo "Ruby version: $$($(RUBY) --version)" + @echo "Project root: $$(pwd)" + @echo "Scripts: $$(ls -1 $(SCRIPTS_DIR)/*.rb | wc -l | tr -d ' ') files" + @echo "Formulae: $$(find $(FORMULA_DIR) -name '*.rb' | wc -l | tr -d ' ') files" + @echo "Theme files: $$(ls -1 $(THEME_DIR)/* | wc -l | tr -d ' ') files" + @if [ -f $(DOCS_DIR)/_data/formulae.json ]; then \ + echo "Generated formulae: $$(cat $(DOCS_DIR)/_data/formulae.json | grep -o '"formulae_count":[0-9]*' | cut -d: -f2)"; \ + fi + +version: ## Show version information + @echo "Homebrew Tap Builder" + @echo "Ruby: $$($(RUBY) --version)" + @echo "Make: $$(make --version | head -1)" + @echo "Git: $$(git --version 2>/dev/null || echo 'not available')" + +# Legacy support (for backward compatibility with scripts/make.rb) +ruby-build: ## Legacy: use Ruby make script for build + @$(RUBY) $(SCRIPTS_DIR)/make.rb build + +ruby-serve: ## Legacy: use Ruby make script for serve + @$(RUBY) $(SCRIPTS_DIR)/make.rb serve + +ruby-clean: ## Legacy: use Ruby make script for clean + @$(RUBY) $(SCRIPTS_DIR)/make.rb clean + +ruby-parse: ## Legacy: use Ruby make script for parse + @$(RUBY) $(SCRIPTS_DIR)/make.rb parse diff --git a/README.md b/README.md index 3e988ea..b4af9b2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ivuorinen/homebrew-tap -A Homebrew tap for ivuorinen's custom formulae with automated documentation. +A Homebrew tap for ivuorinen's custom formulae with automated documentation and dark mode support. -## Usage +## Quick Start ```bash # Add the tap @@ -17,38 +17,156 @@ brew search ivuorinen/homebrew-tap/ ## Documentation -Visit [https://ivuorinen.github.io/homebrew-tap/](https://ivuorinen.github.io/homebrew-tap/) for complete documentation of all available formulae. - -## Available Formulae - -The documentation is automatically generated from the formula files and includes: -- Installation instructions -- Dependencies -- Version information -- Source links +Visit [https://ivuorinen.net/homebrew-tap/](https://ivuorinen.net/homebrew-tap/) for complete documentation with: +- Installation instructions for each formula +- Dependencies and version information +- Source links and SHA256 checksums +- Dark mode support with system preference detection ## Contributing 1. Fork this repository -2. Create a new formula in the `Formula/` directory -3. Follow the [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook) -4. Submit a pull request +2. Create a new formula in the `Formula/` directory following the [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook) +3. Submit a pull request -The CI will automatically validate your formula and update the documentation. +The CI will automatically validate your formula and update the documentation site. ## Development +This tap uses a custom Ruby-based static site generator with zero external dependencies. + +### Commands + +#### Using Make (Recommended) + +The project includes a comprehensive Makefile that provides convenient commands for all build operations: + ```bash -# Install dependencies -bundle install - -# Parse formulae locally -ruby scripts/parse_formulas.rb - -# Serve documentation locally -cd docs && bundle exec jekyll serve +make help # Show all available commands with descriptions +make build # Build the documentation site +make serve # Start development server (http://localhost:4000) +make parse # Parse formulae and generate JSON data only +make clean # Clean all generated files ``` +**Development workflow:** +```bash +make dev # Full development workflow (parse + build + serve) +make setup # Check development environment setup +make test # Run validation tests +make check # Check project structure +make install # Install Ruby dependencies (if Gemfile exists) +make info # Show project information +``` + +**Server options:** +```bash +make serve PORT=3000 # Use port 3000 +make serve HOST=0.0.0.0 # Bind to all interfaces +make serve PORT=8080 HOST=0.0.0.0 # Custom port and host +make serve-all # Start server on all interfaces (0.0.0.0) +make serve-3000 # Quick shortcut for port 3000 +make watch # Alias for serve with auto-rebuild +``` + +**Homebrew-specific targets:** +```bash +make tap-test # Test tap installation locally +make tap-install # Install this tap locally +make formula-new NAME=tool-name # Create new formula template +``` + +**Production and testing:** +```bash +make build-production # Clean build for production deployment +make version # Show version information +``` + +All Makefile targets include helpful status messages and error handling. + +**File Watching and Auto-Rebuild:** + +The development server (`make serve`) includes intelligent file watching that: +- โœ… Only watches source files (Formula/, theme/, scripts/, config files) +- โœ… Excludes generated output files to prevent infinite rebuild loops +- โœ… Includes debouncing to handle multiple rapid file changes +- โœ… Provides clear status messages for rebuild operations + +Files monitored for changes: +- `Formula/**/*.rb` - Homebrew formula files +- `theme/**/*.{css,js,erb,html}` - Theme templates and assets +- `scripts/*.rb` - Build scripts +- `Makefile`, `README.md`, `Gemfile` - Configuration files + +#### Using Ruby Scripts (Alternative) + +```bash +ruby scripts/make.rb build # Build the documentation site +ruby scripts/make.rb serve # Start development server +ruby scripts/make.rb parse # Parse formulae and generate JSON data only +ruby scripts/make.rb clean # Clean all generated files +ruby scripts/make.rb help # Show all available commands +``` + +### How It Works + +The documentation system consists of three main components: + +1. **Formula Parser** - Safely extracts metadata from `.rb` files using regex patterns (no code evaluation) +2. **Site Builder** - Generates static HTML from ERB templates using the parsed data +3. **Development Server** - Serves the site locally with auto-rebuild on file changes + +### Project Structure + +``` +docs/ +โ”œโ”€โ”€ _data/ # Generated JSON data +โ”œโ”€โ”€ assets/ # Copied static assets (fonts, images, etc.) +โ”œโ”€โ”€ formula/ # Individual formula pages (generated) +โ”œโ”€โ”€ index.html # Homepage (generated) +โ””โ”€โ”€ formulae.html # Formula listing (generated) +theme/ +โ”œโ”€โ”€ assets/ # Original assets: fonts, images, etc. +โ”œโ”€โ”€ _command_input.html.erb # Input command snippet partial +โ”œโ”€โ”€ _footer.html.erb # Footer partial +โ”œโ”€โ”€ _formula_card.html.erb # Formula card partial +โ”œโ”€โ”€ _head.html.erb # HTML head partial +โ”œโ”€โ”€ _header.html.erb # Header partial +โ”œโ”€โ”€ _nav.html.erb # Navigation partial +โ”œโ”€โ”€ _nothing_here.html.erb # "No formulae found" partial +โ”œโ”€โ”€ index.html.erb # Homepage template +โ”œโ”€โ”€ formulae.html.erb # Formula listing template +โ”œโ”€โ”€ formula.html.erb # Individual formula template +โ”œโ”€โ”€ style.css # Stylesheets with dark mode support +โ””โ”€โ”€ main.js # Site functionality: search, dark mode toggle, etc. +Formula/ +โ””โ”€โ”€ *.rb # Homebrew formula files +scripts/ +โ”œโ”€โ”€ make.rb # Main build script +โ””โ”€โ”€ parser.rb # Formula parser +Makefile # Build commands and targets +README.md # This documentation +Gemfile # Ruby dependencies +``` + +### Features + +- โœ… Zero external dependencies (Ruby stdlib only) +- โœ… Fast builds with auto-reload development server +- โœ… Dark mode with system preference detection +- โœ… Responsive design with accessibility support +- โœ… GitHub Pages compatible output +- โœ… Automatic deployment via GitHub Actions + +### Customization + +Templates can be customized by editing files in the `theme/` directory: +- `index.html.erb` - Homepage template +- `formulae.html.erb` - Formula listing page +- `formula.html.erb` - Individual formula page template +- `style.css` - Stylesheets and theming +- `main.js` - JavaScript functionality + ## License This tap is released under the MIT License. See LICENSE for details. diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 0b4442e..0000000 --- a/docs/Gemfile +++ /dev/null @@ -1,19 +0,0 @@ -source "https://rubygems.org" - -ruby "3.4.5" - -gem "jekyll", "~> 4.4" - -group :jekyll_plugins do - gem "jekyll-feed", "~> 0.17" - gem "jekyll-seo-tag", "~> 2.8" - gem "jekyll-sitemap", "~> 1.4" -end - -platforms :mingw, :x64_mingw, :mswin, :jruby do - gem "tzinfo", ">= 1", "< 3" - gem "tzinfo-data" -end - -gem "wdm", "~> 0.2", platforms: [:mingw, :x64_mingw, :mswin] -gem "http_parser.rb", "~> 0.6.0", platforms: [:jruby] diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index eb08e82..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,53 +0,0 @@ -title: ivuorinen/homebrew-tap -email: your-email@example.com -description: >- - Homebrew Tap containing custom formulae for various tools and utilities. - Automatically updated documentation for all available formulae. -baseurl: "/homebrew-tap" -url: "https://ivuorinen.github.io" -repository: ivuorinen/homebrew-tap - -markdown: kramdown -kramdown: - input: GFM - syntax_highlighter: rouge - syntax_highlighter_opts: - css_class: 'highlight' - span: - line_numbers: false - block: - line_numbers: true - -plugins: - - jekyll-feed - - jekyll-seo-tag - - jekyll-sitemap - -collections: - formulae: - output: true - permalink: /formula/:name/ - -defaults: - - scope: - path: "" - type: "pages" - values: - layout: "default" - - scope: - path: "" - type: "formulae" - values: - layout: "formula" - -exclude: - - Gemfile - - Gemfile.lock - - node_modules - - vendor/bundle/ - - vendor/cache/ - - vendor/gems/ - - vendor/ruby/ - - scripts/ - - .sass-cache/ - - .jekyll-cache/ diff --git a/docs/_data/.gitkeep b/docs/_data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html deleted file mode 100644 index f400dc9..0000000 --- a/docs/_layouts/default.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - {{ page.title | default: site.title }} - {% seo %} - - - - - -
-
- {{ content }} -
-
- - - - diff --git a/docs/_layouts/formula.html b/docs/_layouts/formula.html deleted file mode 100644 index da596a2..0000000 --- a/docs/_layouts/formula.html +++ /dev/null @@ -1,70 +0,0 @@ ---- -layout: default ---- - -{% assign formula = site.data.formulae.formulae | where: "name", page.formula | first %} - -
-
-

{{ formula.name }}

-
- {% if formula.version %}v{{ formula.version }}{% endif %} - {% if formula.license %}{{ formula.license }}{% endif %} - {% if formula.homepage %}Homepage{% endif %} -
-
- - {% if formula.description %} -
-

{{ formula.description }}

-
- {% endif %} - -
-

Installation

-
-
brew tap {{ site.repository }}
-brew install {{ formula.name }}
-
-
- - {% if formula.dependencies.size > 0 %} -
-

Dependencies

-
    - {% for dep in formula.dependencies %} -
  • {{ dep }}
  • - {% endfor %} -
-
- {% endif %} - -
-

Formula Details

- - {% if formula.url %} - - - - - {% endif %} - {% if formula.sha256 %} - - - - - {% endif %} - - - - -
Source URL{{ formula.url | truncate: 60 }}
SHA256{{ formula.sha256 | truncate: 20 }}...
Last Updated{{ formula.last_modified | date: "%B %d, %Y" }}
-
- -
-

Formula Source

-

- View {{ formula.name }}.rb on GitHub -

-
-
diff --git a/docs/assets/.gitkeep b/docs/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css deleted file mode 100644 index 71c83f2..0000000 --- a/docs/assets/css/style.css +++ /dev/null @@ -1,162 +0,0 @@ -:root { - --primary-color: #0366d6; - --text-color: #24292e; - --bg-color: #ffffff; - --code-bg: #f6f8fa; - --border-color: #e1e4e8; - --success-color: #28a745; - --warning-color: #ffc107; -} - -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; - line-height: 1.6; - color: var(--text-color); - background: var(--bg-color); - margin: 0; - padding: 0; -} - -.wrapper { - max-width: 980px; - margin: 0 auto; - padding: 0 2rem; -} - -.site-header { - border-bottom: 1px solid var(--border-color); - padding: 1rem 0; -} - -.site-header h1 { - margin: 0; - display: inline-block; -} - -.site-header a { - text-decoration: none; - color: var(--text-color); -} - -.site-header nav { - float: right; - margin-top: 0.5rem; -} - -.site-header nav a { - margin-left: 1rem; - color: var(--primary-color); -} - -.page-content { - min-height: 70vh; - padding: 2rem 0; -} - -.formula-page { - max-width: 100%; -} - -.formula-header { - border-bottom: 1px solid var(--border-color); - padding-bottom: 1rem; - margin-bottom: 2rem; -} - -.formula-meta { - display: flex; - gap: 1rem; - margin-top: 0.5rem; - font-size: 0.9rem; - flex-wrap: wrap; -} - -.version { - background: var(--success-color); - color: white; - padding: 0.2rem 0.5rem; - border-radius: 3px; -} - -.license { - background: var(--code-bg); - border: 1px solid var(--border-color); - padding: 0.2rem 0.5rem; - border-radius: 3px; -} - -.code-block { - background: var(--code-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 1rem; - margin: 1rem 0; - overflow-x: auto; -} - -.code-block pre { - margin: 0; -} - -.dep-list { - list-style: none; - padding: 0; -} - -.dep-list li { - padding: 0.5rem; - border-left: 3px solid var(--primary-color); - margin: 0.5rem 0; - background: var(--code-bg); -} - -.formula-details { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -.formula-details th, -.formula-details td { - padding: 0.75rem; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.formula-details th { - background: var(--code-bg); - font-weight: 600; - width: 25%; -} - -.site-footer { - border-top: 1px solid var(--border-color); - padding: 2rem 0; - text-align: center; - color: #586069; - font-size: 0.875rem; -} - -@media (max-width: 768px) { - .wrapper { - padding: 0 1rem; - } - - .site-header nav { - float: none; - margin-top: 1rem; - } - - .formula-meta { - flex-direction: column; - gap: 0.5rem; - } - - .formula-details th { - width: 35%; - } -} diff --git a/docs/formula/.gitkeep b/docs/formula/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/formulae.md b/docs/formulae.md deleted file mode 100644 index 97b0f4a..0000000 --- a/docs/formulae.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: default -title: All Formulae ---- - -# All Formulae - -{% if site.data.formulae.formulae.size > 0 %} -{% for formula in site.data.formulae.formulae %} -## [{{ formula.name }}]({{ '/formula/' | append: formula.name | relative_url }}) - -{% if formula.description %}{{ formula.description }}{% endif %} - -**Installation:** -```bash -brew install {{ formula.name }} -``` - -{% if formula.dependencies.size > 0 %} -**Dependencies:** {{ formula.dependencies | join: ', ' }} -{% endif %} - ---- -{% endfor %} -{% else %} -No formulae available yet. Add some formulae to the `Formula/` directory to get started. -{% endif %} diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index d3fb088..0000000 --- a/docs/index.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -layout: default -title: Home ---- - -# ivuorinen/homebrew-tap - -Welcome to the documentation for ivuorinen's Homebrew tap. This tap contains custom formulae for various tools and utilities. - -## Quick Start - -```bash -brew tap ivuorinen/homebrew-tap -brew install -``` - -## Available Formulae - -{% if site.data.formulae.formulae.size > 0 %} -
-{% for formula in site.data.formulae.formulae %} -
-

{{ formula.name }}

- {% if formula.description %}

{{ formula.description }}

{% endif %} -
- {% if formula.version %}v{{ formula.version }}{% endif %} - {% if formula.license %}{{ formula.license }}{% endif %} -
-
-{% endfor %} -
-{% else %} -

No formulae available yet. Add some formulae to the Formula/ directory to get started.

-{% endif %} - -## Repository - -View the source code and contribute on [GitHub](https://github.com/{{ site.repository }}). - ---- - -*Documentation automatically generated from formula files.* diff --git a/homebrew-tap-scaffold.sh b/homebrew-tap-scaffold.sh deleted file mode 100755 index 89253dc..0000000 --- a/homebrew-tap-scaffold.sh +++ /dev/null @@ -1,1016 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Homebrew Tap Scaffold Script -# Creates a complete Homebrew tap with automated GitHub Pages documentation -# Repository: ivuorinen/homebrew-tap - -REPO_OWNER="ivuorinen" -REPO_NAME="homebrew-tap" -RUBY_VERSION="3.4.5" - -echo "๐Ÿบ Creating Homebrew tap: ${REPO_OWNER}/${REPO_NAME}" - -# Create directory structure -echo "๐Ÿ“ Creating directory structure..." -mkdir -p {Formula,docs/{_data,_includes,_layouts,assets/{css,js}},scripts,.github/workflows} - -# Create Ruby version file -echo "๐Ÿ“ Creating .ruby-version..." -cat >.ruby-version <Gemfile <<'EOF' -source "https://rubygems.org" - -ruby "3.4.5" - -gem "parser", "~> 3.3" -gem "json", "~> 2.7" - -group :development, :test do - gem "rubocop", "~> 1.69" - gem "rubocop-rspec", "~> 3.6" - gem "rubocop-performance", "~> 1.25" - gem "rspec", "~> 3.13" -end -EOF - -# Create formula parser script -echo "๐Ÿ“ Creating formula parser..." -cat >scripts/parse_formulas.rb <<'EOF' -#!/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/homebrew-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 -EOF - -chmod +x scripts/parse_formulas.rb - -# Create GitHub Actions CI workflow -echo "๐Ÿ“ Creating CI workflow..." -cat >.github/workflows/ci.yml <<'EOF' -name: CI -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: read - pull-requests: write - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test-bot: - strategy: - fail-fast: false - matrix: - os: [ubuntu-22.04, macos-13, macos-14] - runs-on: ${{ matrix.os }} - - steps: - - name: Set up Homebrew - id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Cache Homebrew Bundler RubyGems - uses: actions/cache@v4 - with: - path: ${{ steps.set-up-homebrew.outputs.gems-path }} - key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }} - restore-keys: ${{ runner.os }}-rubygems- - - - name: Install Homebrew Bundler RubyGems - run: brew install-bundler-gems - - - name: Run brew test-bot (cleanup) - run: brew test-bot --only-cleanup-before - - - name: Run brew test-bot (setup) - run: brew test-bot --only-setup - - - name: Run brew test-bot (tap syntax) - run: brew test-bot --only-tap-syntax - - - name: Run brew test-bot (formulae) - if: github.event_name == 'pull_request' - run: brew test-bot --only-formulae - - - name: Upload bottles as artifact - if: always() && github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 - with: - name: bottles_${{ matrix.os }} - path: '*.bottle.*' -EOF - -# Create GitHub Pages workflow -echo "๐Ÿ“ Creating Pages workflow..." -cat >.github/workflows/pages-build.yml <<'EOF' -name: Build and Deploy Documentation -on: - push: - branches: [main] - paths: - - 'Formula/**' - - 'docs/**' - - 'scripts/**' - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.4' - bundler-cache: true - - - name: Parse Formulae and Generate Data - run: | - ruby scripts/parse_formulas.rb - echo "Generated formulae.json with $(jq '.formulae | length' docs/_data/formulae.json) formulae" - - - name: Setup Pages - id: pages - uses: actions/configure-pages@v5 - - - name: Build Jekyll Site - run: | - cd docs - bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" - env: - JEKYLL_ENV: production - - - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs/_site - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 -EOF - -# Create Jekyll Gemfile -echo "๐Ÿ“ Creating Jekyll Gemfile..." -cat >docs/Gemfile <<'EOF' -source "https://rubygems.org" - -ruby "3.4.5" - -gem "jekyll", "~> 4.4" - -group :jekyll_plugins do - gem "jekyll-feed", "~> 0.17" - gem "jekyll-seo-tag", "~> 2.8" - gem "jekyll-sitemap", "~> 1.4" -end - -platforms :mingw, :x64_mingw, :mswin, :jruby do - gem "tzinfo", ">= 1", "< 3" - gem "tzinfo-data" -end - -gem "wdm", "~> 0.2", platforms: [:mingw, :x64_mingw, :mswin] -gem "http_parser.rb", "~> 0.6.0", platforms: [:jruby] -EOF - -# Create Jekyll configuration -echo "๐Ÿ“ Creating Jekyll config..." -cat >docs/_config.yml <<'EOF' -title: ivuorinen/homebrew-tap -email: your-email@example.com -description: >- - Homebrew Tap containing custom formulae for various tools and utilities. - Automatically updated documentation for all available formulae. -baseurl: "/homebrew-tap" -url: "https://ivuorinen.github.io" -repository: ivuorinen/homebrew-tap - -markdown: kramdown -kramdown: - input: GFM - syntax_highlighter: rouge - syntax_highlighter_opts: - css_class: 'highlight' - span: - line_numbers: false - block: - line_numbers: true - -plugins: - - jekyll-feed - - jekyll-seo-tag - - jekyll-sitemap - -collections: - formulae: - output: true - permalink: /formula/:name/ - -defaults: - - scope: - path: "" - type: "pages" - values: - layout: "default" - - scope: - path: "" - type: "formulae" - values: - layout: "formula" - -exclude: - - Gemfile - - Gemfile.lock - - node_modules - - vendor/bundle/ - - vendor/cache/ - - vendor/gems/ - - vendor/ruby/ - - scripts/ - - .sass-cache/ - - .jekyll-cache/ -EOF - -# Create default layout -echo "๐Ÿ“ Creating Jekyll layouts..." -cat >docs/_layouts/default.html <<'EOF' - - - - - - {{ page.title | default: site.title }} - {% seo %} - - - - - -
-
- {{ content }} -
-
- -
-
-

© {{ 'now' | date: '%Y' }} {{ site.title }}. Built with Jekyll and GitHub Pages.

-
-
- - -EOF - -# Create formula layout -cat >docs/_layouts/formula.html <<'EOF' ---- -layout: default ---- - -{% assign formula = site.data.formulae.formulae | where: "name", page.formula | first %} - -
-
-

{{ formula.name }}

-
- {% if formula.version %}v{{ formula.version }}{% endif %} - {% if formula.license %}{{ formula.license }}{% endif %} - {% if formula.homepage %}Homepage{% endif %} -
-
- - {% if formula.description %} -
-

{{ formula.description }}

-
- {% endif %} - -
-

Installation

-
-
brew tap {{ site.repository }}
-brew install {{ formula.name }}
-
-
- - {% if formula.dependencies.size > 0 %} -
-

Dependencies

-
    - {% for dep in formula.dependencies %} -
  • {{ dep }}
  • - {% endfor %} -
-
- {% endif %} - -
-

Formula Details

- - {% if formula.url %} - - - - - {% endif %} - {% if formula.sha256 %} - - - - - {% endif %} - - - - -
Source URL{{ formula.url | truncate: 60 }}
SHA256{{ formula.sha256 | truncate: 20 }}...
Last Updated{{ formula.last_modified | date: "%B %d, %Y" }}
-
- -
-

Formula Source

-

- View {{ formula.name }}.rb on GitHub -

-
-
-EOF - -# Create main stylesheet -echo "๐Ÿ“ Creating CSS..." -cat >docs/assets/css/style.css <<'EOF' -:root { - --primary-color: #0366d6; - --text-color: #24292e; - --bg-color: #ffffff; - --code-bg: #f6f8fa; - --border-color: #e1e4e8; - --success-color: #28a745; - --warning-color: #ffc107; -} - -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; - line-height: 1.6; - color: var(--text-color); - background: var(--bg-color); - margin: 0; - padding: 0; -} - -.wrapper { - max-width: 980px; - margin: 0 auto; - padding: 0 2rem; -} - -.site-header { - border-bottom: 1px solid var(--border-color); - padding: 1rem 0; -} - -.site-header h1 { - margin: 0; - display: inline-block; -} - -.site-header a { - text-decoration: none; - color: var(--text-color); -} - -.site-header nav { - float: right; - margin-top: 0.5rem; -} - -.site-header nav a { - margin-left: 1rem; - color: var(--primary-color); -} - -.page-content { - min-height: 70vh; - padding: 2rem 0; -} - -.formula-page { - max-width: 100%; -} - -.formula-header { - border-bottom: 1px solid var(--border-color); - padding-bottom: 1rem; - margin-bottom: 2rem; -} - -.formula-meta { - display: flex; - gap: 1rem; - margin-top: 0.5rem; - font-size: 0.9rem; - flex-wrap: wrap; -} - -.version { - background: var(--success-color); - color: white; - padding: 0.2rem 0.5rem; - border-radius: 3px; -} - -.license { - background: var(--code-bg); - border: 1px solid var(--border-color); - padding: 0.2rem 0.5rem; - border-radius: 3px; -} - -.code-block { - background: var(--code-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 1rem; - margin: 1rem 0; - overflow-x: auto; -} - -.code-block pre { - margin: 0; -} - -.dep-list { - list-style: none; - padding: 0; -} - -.dep-list li { - padding: 0.5rem; - border-left: 3px solid var(--primary-color); - margin: 0.5rem 0; - background: var(--code-bg); -} - -.formula-details { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -.formula-details th, -.formula-details td { - padding: 0.75rem; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.formula-details th { - background: var(--code-bg); - font-weight: 600; - width: 25%; -} - -.site-footer { - border-top: 1px solid var(--border-color); - padding: 2rem 0; - text-align: center; - color: #586069; - font-size: 0.875rem; -} - -@media (max-width: 768px) { - .wrapper { - padding: 0 1rem; - } - - .site-header nav { - float: none; - margin-top: 1rem; - } - - .formula-meta { - flex-direction: column; - gap: 0.5rem; - } - - .formula-details th { - width: 35%; - } -} -EOF - -# Create main index page -echo "๐Ÿ“ Creating index page..." -cat >docs/index.md <<'EOF' ---- -layout: default -title: Home ---- - -# ivuorinen/homebrew-tap - -Welcome to the documentation for ivuorinen's Homebrew tap. This tap contains custom formulae for various tools and utilities. - -## Quick Start - -```bash -brew tap ivuorinen/homebrew-tap -brew install -``` - -## Available Formulae - -{% if site.data.formulae.formulae.size > 0 %} -
-{% for formula in site.data.formulae.formulae %} -
-

{{ formula.name }}

- {% if formula.description %}

{{ formula.description }}

{% endif %} -
- {% if formula.version %}v{{ formula.version }}{% endif %} - {% if formula.license %}{{ formula.license }}{% endif %} -
-
-{% endfor %} -
-{% else %} -

No formulae available yet. Add some formulae to the Formula/ directory to get started.

-{% endif %} - -## Repository - -View the source code and contribute on [GitHub](https://github.com/{{ site.repository }}). - ---- - -*Documentation automatically generated from formula files.* -EOF - -# Create formulae listing page -cat >docs/formulae.md <<'EOF' ---- -layout: default -title: All Formulae ---- - -# All Formulae - -{% if site.data.formulae.formulae.size > 0 %} -{% for formula in site.data.formulae.formulae %} -## [{{ formula.name }}]({{ '/formula/' | append: formula.name | relative_url }}) - -{% if formula.description %}{{ formula.description }}{% endif %} - -**Installation:** -```bash -brew install {{ formula.name }} -``` - -{% if formula.dependencies.size > 0 %} -**Dependencies:** {{ formula.dependencies | join: ', ' }} -{% endif %} - ---- -{% endfor %} -{% else %} -No formulae available yet. Add some formulae to the `Formula/` directory to get started. -{% endif %} -EOF - -# Create RuboCop configuration -echo "๐Ÿ“ Creating RuboCop config..." -cat >.rubocop.yml <<'EOF' -AllCops: - TargetRubyVersion: 3.4 - NewCops: enable - Exclude: - - 'vendor/**/*' - - 'docs/**/*' - - '.bundle/**/*' - -Layout/LineLength: - Max: 120 - AllowedPatterns: - - '^\s*#' - - 'url "' - -Layout/IndentationStyle: - EnforcedStyle: spaces - -Layout/IndentationWidth: - Width: 2 - -Style/StringLiterals: - EnforcedStyle: double_quotes - -Style/FrozenStringLiteralComment: - Enabled: true - EnforcedStyle: always - -Naming/FileName: - Exclude: - - 'Formula/**/*.rb' - -Metrics/MethodLength: - Max: 30 - -Metrics/ClassLength: - Max: 150 - -Metrics/BlockLength: - Exclude: - - 'spec/**/*' - - '*.gemspec' -EOF - -# Create Dependabot configuration -echo "๐Ÿ“ Creating Dependabot config..." -cat >.github/dependabot.yml <<'EOF' -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - groups: - github-actions: - patterns: - - "actions/*" - - "ruby/setup-ruby" - - "Homebrew/actions/*" - commit-message: - prefix: "ci" - include: "scope" - - - package-ecosystem: "bundler" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - groups: - development-dependencies: - dependency-type: "development" - patterns: - - "rubocop*" - - "rspec*" - commit-message: - prefix: "deps" - include: "scope" -EOF - -# Create example formula -echo "๐Ÿ“ Creating example formula..." -mkdir -p Formula/e -cat >Formula/e/example-tool.rb <<'EOF' -class ExampleTool < Formula - desc "An example tool to demonstrate the tap functionality" - homepage "https://github.com/ivuorinen/example-tool" - url "https://github.com/ivuorinen/example-tool/archive/v1.0.0.tar.gz" - sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - license "MIT" - - depends_on "go" => :build - - def install - system "go", "build", *std_go_args(ldflags: "-s -w") - end - - test do - assert_match "example-tool version 1.0.0", shell_output("#{bin}/example-tool --version") - end -end -EOF - -# Create README -echo "๐Ÿ“ Creating README..." -cat >README.md <<'EOF' -# ivuorinen/homebrew-tap - -A Homebrew tap for ivuorinen's custom formulae with automated documentation. - -## Usage - -```bash -# Add the tap -brew tap ivuorinen/homebrew-tap - -# Install a formula -brew install - -# List available formulae -brew search ivuorinen/homebrew-tap/ -``` - -## Documentation - -Visit [https://ivuorinen.github.io/homebrew-tap/](https://ivuorinen.github.io/homebrew-tap/) for complete documentation of all available formulae. - -## Available Formulae - -The documentation is automatically generated from the formula files and includes: -- Installation instructions -- Dependencies -- Version information -- Source links - -## Contributing - -1. Fork this repository -2. Create a new formula in the `Formula/` directory -3. Follow the [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook) -4. Submit a pull request - -The CI will automatically validate your formula and update the documentation. - -## Development - -```bash -# Install dependencies -bundle install - -# Parse formulae locally -ruby scripts/parse_formulas.rb - -# Serve documentation locally -cd docs && bundle exec jekyll serve -``` - -## License - -This tap is released under the MIT License. See LICENSE for details. -EOF - -# Create .gitignore -echo "๐Ÿ“ Creating .gitignore..." -cat >.gitignore <<'EOF' -# Ruby -Gemfile.lock -.bundle/ -vendor/bundle/ - -# Jekyll -docs/_site/ -docs/.sass-cache/ -docs/.jekyll-cache/ -docs/.jekyll-metadata - -# macOS -.DS_Store -.AppleDouble -.LSOverride - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Logs -*.log - -# Generated files -docs/_data/formulae.json -EOF - -# Create LICENSE -echo "๐Ÿ“ Creating LICENSE..." -cat >LICENSE <<'EOF' -MIT License - -Copyright (c) 2025 Ismo Vuorinen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -EOF - -# Generate initial documentation -echo "๐Ÿ”„ Generating initial documentation..." -if command -v ruby >/dev/null 2>&1; then - ruby scripts/parse_formulas.rb - echo "โœ… Initial documentation generated" -else - echo "โš ๏ธ Ruby not found. Documentation will be generated in CI." -fi - -# Initialize git repository if not already initialized -if [ ! -d .git ]; then - echo "๐Ÿ”„ Initializing git repository..." - git init - git add . - git commit -m "Initial Homebrew tap setup with automated documentation - -- Add formula parser with Ruby AST parsing -- Add GitHub Actions CI/CD workflows -- Add Jekyll-based documentation site -- Add RuboCop and Dependabot configuration -- Add example formula for demonstration" - echo "โœ… Git repository initialized" -else - echo "โ„น๏ธ Git repository already exists" -fi - -# Final instructions -echo "" -echo "๐ŸŽ‰ Homebrew tap scaffold complete!" -echo "" -echo "Next steps:" -echo "1. Push to GitHub: git remote add origin https://github.com/ivuorinen/homebrew-tap.git" -echo "2. Enable GitHub Pages in repository settings (Source: GitHub Actions)" -echo "3. Add your formulae to the Formula/ directory" -echo "4. The documentation will update automatically on each push" -echo "" -echo "Local development:" -echo "- Run 'bundle install' to install dependencies" -echo "- Run 'ruby scripts/parse_formulas.rb' to update documentation" -echo "- Run 'cd docs && bundle install && bundle exec jekyll serve' for local preview" -echo "" -echo "Documentation will be available at: https://ivuorinen.github.io/homebrew-tap/" -EOF diff --git a/scripts/build_site.rb b/scripts/build_site.rb new file mode 100644 index 0000000..2cca69a --- /dev/null +++ b/scripts/build_site.rb @@ -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 diff --git a/scripts/make.rb b/scripts/make.rb new file mode 100644 index 0000000..e1b9a61 --- /dev/null +++ b/scripts/make.rb @@ -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 " + 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 diff --git a/scripts/parse_formulas.rb b/scripts/parse_formulas.rb index cde5d29..0c0f595 100755 --- a/scripts/parse_formulas.rb +++ b/scripts/parse_formulas.rb @@ -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 diff --git a/scripts/serve.rb b/scripts/serve.rb new file mode 100644 index 0000000..93feddd --- /dev/null +++ b/scripts/serve.rb @@ -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 diff --git a/theme/_command_input.html.erb b/theme/_command_input.html.erb new file mode 100644 index 0000000..af143ab --- /dev/null +++ b/theme/_command_input.html.erb @@ -0,0 +1,10 @@ +
+ +<%= h(label || 'CLI') %> +
diff --git a/theme/_footer.html.erb b/theme/_footer.html.erb new file mode 100644 index 0000000..2dd4c46 --- /dev/null +++ b/theme/_footer.html.erb @@ -0,0 +1,7 @@ +
+

+ +

+
diff --git a/theme/_formula_card.html.erb b/theme/_formula_card.html.erb new file mode 100644 index 0000000..c56014e --- /dev/null +++ b/theme/_formula_card.html.erb @@ -0,0 +1,55 @@ +<% +# Required parameters: +# - formula: The formula hash object +# - base_path: Path prefix for formula links (e.g., '' or '../') +# Optional parameters: +# - show_homepage: Show homepage link (default: false) +# - show_dependencies: Show dependency badges (default: false) +# - show_last_modified: Show last modified time (default: false) + +formula_name = formula['name'].to_s +return if formula_name.empty? +formula_slug = formula_name.gsub(/[^a-z0-9._-]/i, '-') +formula_slug = formula_name if formula_slug.empty? +formula_description = formula['description'].to_s +# Check if methods exist (they're defined as singleton methods in PartialContext) +show_homepage = respond_to?(:show_homepage) ? self.show_homepage : false +show_dependencies = respond_to?(:show_dependencies) ? self.show_dependencies : false +show_last_modified = respond_to?(:show_last_modified) ? self.show_last_modified : false +%> +
+
+

+ <%= h(formula_name) %> +

+

<%= h(formula_description) %>

+ <% if show_homepage && formula['homepage'] %> +

+ + Homepage + + +

+ <% end %> +
+ + <% if show_last_modified && formula['last_modified'] %> +
+ Updated <%= format_relative_time(formula['last_modified']) %> +
+ <% end %> + +
+ <% if formula['license'] %> + License: <%= h(formula['license'].to_s) %> + <% end %> + <% if formula['version'] %> + v<%= h(formula['version'].to_s) %> + <% end %> + <% if show_dependencies %> + <% Array(formula['dependencies']).compact.each do |dep| %> + dep: <%= h(dep.to_s) %> + <% end %> + <% end %> +
+
diff --git a/theme/_head.html.erb b/theme/_head.html.erb new file mode 100644 index 0000000..61a60b2 --- /dev/null +++ b/theme/_head.html.erb @@ -0,0 +1,31 @@ + + + + <% page_title = title.to_s %> + <%= h(page_title) %> + <% stylesheet = style_path || 'style.css' %> + + + + + diff --git a/theme/_header.html.erb b/theme/_header.html.erb new file mode 100644 index 0000000..3aaa76a --- /dev/null +++ b/theme/_header.html.erb @@ -0,0 +1,15 @@ +
+ + +
+
+
+

<%= h(tap_name) %>

+

<%= h(description) %>

+
+ +
+
+
diff --git a/theme/_nav.html.erb b/theme/_nav.html.erb new file mode 100644 index 0000000..60cfa39 --- /dev/null +++ b/theme/_nav.html.erb @@ -0,0 +1,13 @@ +<% nav_base = "rounded-full px-3 py-1 transition hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-white" %> +<% nav_active = "bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900" %> +<% active_tab = (active || :none).to_sym rescue :none %> +<% prefix = base_path.to_s %> +<% href = lambda do |page| + prefix.empty? ? page : File.join(prefix, page) +end %> + diff --git a/theme/_nothing_here.html.erb b/theme/_nothing_here.html.erb new file mode 100644 index 0000000..a1ca16b --- /dev/null +++ b/theme/_nothing_here.html.erb @@ -0,0 +1,6 @@ +
+
+

No formulae available

+

This tap currently has no formulae. Please check back later.

+
+
diff --git a/theme/assets/MonaspaceArgonVar.woff b/theme/assets/MonaspaceArgonVar.woff new file mode 100644 index 0000000..5bde29e Binary files /dev/null and b/theme/assets/MonaspaceArgonVar.woff differ diff --git a/theme/assets/MonaspaceArgonVar.woff2 b/theme/assets/MonaspaceArgonVar.woff2 new file mode 100644 index 0000000..706aa5d Binary files /dev/null and b/theme/assets/MonaspaceArgonVar.woff2 differ diff --git a/theme/formula.html.erb b/theme/formula.html.erb new file mode 100644 index 0000000..8c57675 --- /dev/null +++ b/theme/formula.html.erb @@ -0,0 +1,111 @@ + + +<% raw_tap_name = @data['tap_name'].to_s %> +<% tap_name = raw_tap_name.empty? ? 'ivuorinen/tap' : raw_tap_name %> +<% formula_name = @formula['name'].to_s %> +<% name = h("#{tap_name}/#{formula_name}") %> +<%= render_partial('head', title: "#{formula_name} - #{tap_name}", style_path: '../style.css') %> + +
+ <%= render_partial('header', tap_name: name, description: @formula['description'].to_s, active_page: :formulae, base_path: '../') %> + +
+
+
+

Installation

+
+ <% if @formula['url'] %> + + + + + + + + <% end %> + <% if @formula['homepage'] %> + + + + + + <% end %> +
+
+ <%= render_partial('command_input', command: "brew install #{name}", size: 'lg', label: 'CLI') %> +
+ +
+
+ + + + <% if @formula['version'] %> + + + + + <% end %> + <% if @formula['license'] %> + + + + + <% end %> + <% if @formula['homepage'] %> + + + + + <% end %> + <% dependencies = Array(@formula['dependencies']).reject(&:nil?) %> + <% if dependencies.any? %> + + + + + <% end %> + + + + + + + + + <% if @formula['sha256'] %> + + + + + <% end %> + +
Formula Details
Version<%= h(@formula['version'].to_s) %>
License<%= h(@formula['license'].to_s) %>
Homepage + + <%= h(@formula['homepage'].to_s) %> + + +
Dependencies +
+ <% dependencies.each do |dep| %> + dep: <%= h(dep.to_s) %> + <% end %> +
+
Formula File<%= h(@formula['file_path'].to_s) %>
Last Modified<%= h(@formula['last_modified'].to_s) %>
SHA256 + +
+
+
+
+
+ + <%= render_partial('footer', generated_at: @data['generated_at']) %> + + + diff --git a/theme/formulae.html.erb b/theme/formulae.html.erb new file mode 100644 index 0000000..0e44e5a --- /dev/null +++ b/theme/formulae.html.erb @@ -0,0 +1,46 @@ + + +<% raw_tap_name = @data['tap_name'].to_s %> +<% tap_name = raw_tap_name.empty? ? 'ivuorinen/tap' : raw_tap_name %> +<%= render_partial('head', title: "All Formulae - #{tap_name}", style_path: 'style.css') %> + +
+ <%= render_partial('header', tap_name: tap_name, description: 'Homebrew Tap containing custom formulae for various tools and utilities.', active_page: :formulae, base_path: '') %> + + <% if @data['formulae_count'].to_i == 0 %> + <%= render_partial('nothing_here') %> + <% else %> + +
+
+
+

All Formulae

+

<%= h(@data['formulae_count'].to_s) %> total

+
+
+ +
+ +
+ <% + # Sort all formulae alphabetically by name + @data['formulae'].sort_by { |f| f['name'].to_s.downcase }.each do |formula| + %> + <%= render_partial('formula_card', formula: formula, base_path: '', show_homepage: true, show_dependencies: true, show_last_modified: true) %> + <% end %> +
+
+
+ + <% end %> + + <%= render_partial('footer', generated_at: @data['generated_at']) %> +
+ + + diff --git a/theme/index.html.erb b/theme/index.html.erb new file mode 100644 index 0000000..d7669a4 --- /dev/null +++ b/theme/index.html.erb @@ -0,0 +1,49 @@ + + +<% raw_tap_name = @data['tap_name'].to_s %> +<% safe_tap_name = raw_tap_name.empty? ? 'ivuorinen/homebrew-tap' : raw_tap_name %> +<%= render_partial('head', title: safe_tap_name, style_path: 'style.css') %> + +
+ <%= render_partial('header', tap_name: safe_tap_name, description: 'Homebrew Tap containing custom formulae for various tools and utilities.', active_page: :home, base_path: '') %> + + <% if @data['formulae_count'].to_i == 0 %> + <%= render_partial('nothing_here') %> + <% else %> + +
+
+

Quick Start

+ <%= render_partial('command_input', command: "brew tap #{safe_tap_name}", size: 'lg', label: 'CLI') %> +
+ +
+ + +
+ <% + # Sort formulae by last_modified date (most recent first) and take top 6 + sorted_formulae = Array(@data['formulae']) + .select { |f| f.is_a?(Hash) && f['last_modified'] } + .sort_by { |f| f['last_modified'] } + .reverse + .first(6) + + sorted_formulae.each do |formula| + %> + <%= render_partial('formula_card', formula: formula, base_path: '', show_last_modified: true) %> + <% end %> +
+
+
+ <% end %> + + + <%= render_partial('footer', generated_at: @data['generated_at'].to_s) %> +
+ + + diff --git a/theme/main.js b/theme/main.js new file mode 100644 index 0000000..98bd76c --- /dev/null +++ b/theme/main.js @@ -0,0 +1,156 @@ +// Dark mode toggle functionality +(() => { + const STORAGE_KEY = "theme"; + + function getStoredTheme() { + try { + return localStorage.getItem(STORAGE_KEY); + } catch { + return null; + } + } + + function setStoredTheme(theme) { + try { + localStorage.setItem(STORAGE_KEY, theme); + } catch { + // Ignore storage failures + } + } + + function getSystemTheme() { + return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + + function getCurrentTheme() { + return getStoredTheme() || getSystemTheme() || "light"; + } + + function applyTheme(theme) { + document.documentElement.classList.toggle("dark", theme === "dark"); + const toggle = document.querySelector(".theme-toggle"); + if (toggle) { + toggle.innerHTML = theme === "dark" ? "โ˜€๏ธ" : "๐ŸŒ™"; + toggle.setAttribute( + "aria-label", + theme === "dark" ? "Switch to light mode" : "Switch to dark mode", + ); + } + } + + function toggleTheme() { + const currentTheme = getCurrentTheme(); + const newTheme = currentTheme === "dark" ? "light" : "dark"; + setStoredTheme(newTheme); + applyTheme(newTheme); + } + + // Watch for system theme changes + const mediaQuery = window.matchMedia?.("(prefers-color-scheme: dark)"); + mediaQuery?.addEventListener("change", (e) => { + if (!getStoredTheme()) { + applyTheme(e.matches ? "dark" : "light"); + } + }); + + // Theme toggle click handler + document.addEventListener("click", (e) => { + if (e.target.closest(".theme-toggle")) { + e.preventDefault(); + toggleTheme(); + } + }); + + // Initialize theme + applyTheme(getCurrentTheme()); + window.toggleTheme = toggleTheme; +})(); + +// Click-to-copy functionality for command inputs +(() => { + async function copyToClipboard(input) { + input.select(); + input.setSelectionRange(0, 99999); + + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(input.value); + return true; + } + } catch { + // Keep text selected for manual copy + } + return false; + } + + function showCopyFeedback(element, success) { + if (!success) return; + + const label = element.closest(".install-cmd")?.querySelector("span"); + if (!label) return; + + const originalText = label.textContent; + label.textContent = "Copied!"; + label.style.color = "#10b981"; + + setTimeout(() => { + label.textContent = originalText; + label.style.color = ""; + }, 1500); + } + + // Setup copy handlers using event delegation + document.addEventListener("click", async (e) => { + const input = e.target.closest('input[readonly][onclick*="select"]'); + if (input) { + input.removeAttribute("onclick"); + const success = await copyToClipboard(input); + showCopyFeedback(input, success); + } + }); +})(); + +// Formula search functionality +(() => { + let searchTimeout = null; + const searchInput = document.getElementById("formula-search"); + if (!searchInput) return; + + function fuzzyMatch(needle, haystack) { + if (!needle) return true; + + needle = needle.toLowerCase(); + haystack = haystack.toLowerCase(); + + let j = 0; + for (let i = 0; i < needle.length; i++) { + const char = needle[i]; + j = haystack.indexOf(char, j); + if (j === -1) return false; + j++; + } + return true; + } + + function performSearch() { + const searchTerm = searchInput.value.trim(); + const cards = document.querySelectorAll(".formula-card"); + + cards.forEach((card) => { + const text = `${card.querySelector("h3")?.textContent || ""} ${card.querySelector("p")?.textContent || ""}`; + card.style.display = !searchTerm || fuzzyMatch(searchTerm, text) ? "" : "none"; + }); + } + searchInput.addEventListener("input", () => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(performSearch, 300); + }); + + searchInput.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + e.preventDefault(); + searchInput.value = ""; + performSearch(); + } + }); +})(); diff --git a/theme/style.css b/theme/style.css new file mode 100644 index 0000000..288e048 --- /dev/null +++ b/theme/style.css @@ -0,0 +1,43 @@ +@font-face { + font-family: "Monaspace Argon"; + src: + url("assets/MonaspaceArgonVar.woff2") format("woff2"), + url("assets/MonaspaceArgonVar.woff") format("woff"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +:root { + color-scheme: light dark; +} + +html { + font-family: + "Monaspace Argon", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Helvetica, + Arial, + sans-serif; +} + +body { + font-family: + "Monaspace Argon", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Helvetica, + Arial, + sans-serif; +} + +code, +pre { + font-family: "Monaspace Argon", monospace; + font-feature-settings: + "calt", "ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09", "ss10", + "liga"; +}