mirror of
https://github.com/ivuorinen/homebrew-tap.git
synced 2026-03-21 22:03:59 +00:00
feat: full site
This commit is contained in:
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
name: CI
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -5,23 +6,25 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-bot:
|
test-bot:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-22.04, macos-13, macos-14]
|
os: [ubuntu-22.04, macos-14]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Homebrew
|
- name: Set up Homebrew
|
||||||
id: set-up-homebrew
|
id: set-up-homebrew
|
||||||
@@ -55,4 +58,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: bottles_${{ matrix.os }}
|
name: bottles_${{ matrix.os }}
|
||||||
path: '*.bottle.*'
|
path: "*.bottle.*"
|
||||||
|
|||||||
39
.github/workflows/pages-build.yml
vendored
39
.github/workflows/pages-build.yml
vendored
@@ -1,17 +1,16 @@
|
|||||||
|
---
|
||||||
name: Build and Deploy Documentation
|
name: Build and Deploy Documentation
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'Formula/**'
|
- "Formula/**"
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
- 'scripts/**'
|
- "scripts/**"
|
||||||
|
- "theme/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions: read-all
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pages
|
group: pages
|
||||||
@@ -20,6 +19,11 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
@@ -28,30 +32,21 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
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: |
|
run: |
|
||||||
ruby scripts/parse_formulas.rb
|
ruby scripts/parse_formulas.rb
|
||||||
echo "Generated formulae.json with $(jq '.formulae | length' docs/_data/formulae.json) formulae"
|
echo "Generated formulae.json with $(jq '.formulae | length' docs/_data/formulae.json) formulae"
|
||||||
|
ruby scripts/build_site.rb
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
id: pages
|
id: pages
|
||||||
uses: actions/configure-pages@v5
|
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
|
- name: Upload Pages Artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: docs/_site
|
path: docs
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
environment:
|
environment:
|
||||||
@@ -59,6 +54,12 @@ jobs:
|
|||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -3,11 +3,13 @@ Gemfile.lock
|
|||||||
.bundle/
|
.bundle/
|
||||||
vendor/bundle/
|
vendor/bundle/
|
||||||
|
|
||||||
# Jekyll
|
# Generated site files
|
||||||
docs/_site/
|
docs/**/*.html
|
||||||
docs/.sass-cache/
|
docs/*.css
|
||||||
docs/.jekyll-cache/
|
docs/*.js
|
||||||
docs/.jekyll-metadata
|
docs/_data/formulae.json
|
||||||
|
docs/assets/*
|
||||||
|
!docs/assets/.gitkeep
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -23,5 +25,5 @@ docs/.jekyll-metadata
|
|||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Generated files
|
# Other
|
||||||
docs/_data/formulae.json
|
AGENTS.md
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.4.5
|
3.3.6
|
||||||
|
|||||||
17
Formula/e/example-tool2.rbx
Normal file
17
Formula/e/example-tool2.rbx
Normal file
@@ -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
|
||||||
176
Makefile
Normal file
176
Makefile
Normal file
@@ -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 <target>"
|
||||||
|
@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
|
||||||
164
README.md
164
README.md
@@ -1,8 +1,8 @@
|
|||||||
# ivuorinen/homebrew-tap
|
# 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
|
```bash
|
||||||
# Add the tap
|
# Add the tap
|
||||||
@@ -17,38 +17,156 @@ brew search ivuorinen/homebrew-tap/
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Visit [https://ivuorinen.github.io/homebrew-tap/](https://ivuorinen.github.io/homebrew-tap/) for complete documentation of all available formulae.
|
Visit [https://ivuorinen.net/homebrew-tap/](https://ivuorinen.net/homebrew-tap/) for complete documentation with:
|
||||||
|
- Installation instructions for each formula
|
||||||
## Available Formulae
|
- Dependencies and version information
|
||||||
|
- Source links and SHA256 checksums
|
||||||
The documentation is automatically generated from the formula files and includes:
|
- Dark mode support with system preference detection
|
||||||
- Installation instructions
|
|
||||||
- Dependencies
|
|
||||||
- Version information
|
|
||||||
- Source links
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork this repository
|
1. Fork this repository
|
||||||
2. Create a new formula in the `Formula/` directory
|
2. Create a new formula in the `Formula/` directory following the [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook)
|
||||||
3. Follow the [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook)
|
3. Submit a pull request
|
||||||
4. 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
|
## 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
|
```bash
|
||||||
# Install dependencies
|
make help # Show all available commands with descriptions
|
||||||
bundle install
|
make build # Build the documentation site
|
||||||
|
make serve # Start development server (http://localhost:4000)
|
||||||
# Parse formulae locally
|
make parse # Parse formulae and generate JSON data only
|
||||||
ruby scripts/parse_formulas.rb
|
make clean # Clean all generated files
|
||||||
|
|
||||||
# Serve documentation locally
|
|
||||||
cd docs && bundle exec jekyll serve
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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
|
## License
|
||||||
|
|
||||||
This tap is released under the MIT License. See LICENSE for details.
|
This tap is released under the MIT License. See LICENSE for details.
|
||||||
|
|||||||
0
docs/.gitkeep
Normal file
0
docs/.gitkeep
Normal file
19
docs/Gemfile
19
docs/Gemfile
@@ -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]
|
|
||||||
@@ -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/
|
|
||||||
0
docs/_data/.gitkeep
Normal file
0
docs/_data/.gitkeep
Normal file
@@ -1,34 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ page.title | default: site.title }}</title>
|
|
||||||
{% seo %}
|
|
||||||
<link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="site-header">
|
|
||||||
<div class="wrapper">
|
|
||||||
<h1><a href="{{ '/' | relative_url }}">{{ site.title }}</a></h1>
|
|
||||||
<nav>
|
|
||||||
<a href="{{ '/' | relative_url }}">Home</a>
|
|
||||||
<a href="{{ '/formulae' | relative_url }}">Formulae</a>
|
|
||||||
<a href="{{ site.repository | prepend: 'https://github.com/' }}">GitHub</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="page-content">
|
|
||||||
<div class="wrapper">
|
|
||||||
{{ content }}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="site-footer">
|
|
||||||
<div class="wrapper">
|
|
||||||
<p>© {{ 'now' | date: '%Y' }} {{ site.title }}. Built with Jekyll and GitHub Pages.</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
---
|
|
||||||
|
|
||||||
{% assign formula = site.data.formulae.formulae | where: "name", page.formula | first %}
|
|
||||||
|
|
||||||
<article class="formula-page">
|
|
||||||
<header class="formula-header">
|
|
||||||
<h1>{{ formula.name }}</h1>
|
|
||||||
<div class="formula-meta">
|
|
||||||
{% if formula.version %}<span class="version">v{{ formula.version }}</span>{% endif %}
|
|
||||||
{% if formula.license %}<span class="license">{{ formula.license }}</span>{% endif %}
|
|
||||||
{% if formula.homepage %}<a href="{{ formula.homepage }}" class="homepage">Homepage</a>{% endif %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% if formula.description %}
|
|
||||||
<section class="description">
|
|
||||||
<p>{{ formula.description }}</p>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="installation">
|
|
||||||
<h2>Installation</h2>
|
|
||||||
<div class="code-block">
|
|
||||||
<pre><code>brew tap {{ site.repository }}
|
|
||||||
brew install {{ formula.name }}</code></pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% if formula.dependencies.size > 0 %}
|
|
||||||
<section class="dependencies">
|
|
||||||
<h2>Dependencies</h2>
|
|
||||||
<ul class="dep-list">
|
|
||||||
{% for dep in formula.dependencies %}
|
|
||||||
<li>{{ dep }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="details">
|
|
||||||
<h2>Formula Details</h2>
|
|
||||||
<table class="formula-details">
|
|
||||||
{% if formula.url %}
|
|
||||||
<tr>
|
|
||||||
<th>Source URL</th>
|
|
||||||
<td><a href="{{ formula.url }}">{{ formula.url | truncate: 60 }}</a></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if formula.sha256 %}
|
|
||||||
<tr>
|
|
||||||
<th>SHA256</th>
|
|
||||||
<td><code>{{ formula.sha256 | truncate: 20 }}...</code></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<th>Last Updated</th>
|
|
||||||
<td>{{ formula.last_modified | date: "%B %d, %Y" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="source">
|
|
||||||
<h2>Formula Source</h2>
|
|
||||||
<p><a href="{{ site.repository | prepend: 'https://github.com/' }}/blob/main/Formula/{{ formula.file_path }}">
|
|
||||||
View {{ formula.name }}.rb on GitHub
|
|
||||||
</a></p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
0
docs/assets/.gitkeep
Normal file
0
docs/assets/.gitkeep
Normal file
@@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
docs/formula/.gitkeep
Normal file
0
docs/formula/.gitkeep
Normal file
@@ -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 %}
|
|
||||||
@@ -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 <formula-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Formulae
|
|
||||||
|
|
||||||
{% if site.data.formulae.formulae.size > 0 %}
|
|
||||||
<div class="formulae-grid">
|
|
||||||
{% for formula in site.data.formulae.formulae %}
|
|
||||||
<div class="formula-card">
|
|
||||||
<h3><a href="{{ '/formula/' | append: formula.name | relative_url }}">{{ formula.name }}</a></h3>
|
|
||||||
{% if formula.description %}<p>{{ formula.description }}</p>{% endif %}
|
|
||||||
<div class="formula-meta">
|
|
||||||
{% if formula.version %}<span class="version">v{{ formula.version }}</span>{% endif %}
|
|
||||||
{% if formula.license %}<span class="license">{{ formula.license }}</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>No formulae available yet. Add some formulae to the <code>Formula/</code> directory to get started.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Repository
|
|
||||||
|
|
||||||
View the source code and contribute on [GitHub](https://github.com/{{ site.repository }}).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Documentation automatically generated from formula files.*
|
|
||||||
File diff suppressed because it is too large
Load Diff
333
scripts/build_site.rb
Normal file
333
scripts/build_site.rb
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
require 'fileutils'
|
||||||
|
require 'erb'
|
||||||
|
require 'pathname'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
# Simple static site generator for homebrew tap documentation
|
||||||
|
class SiteBuilder
|
||||||
|
include ERB::Util
|
||||||
|
class PartialContext
|
||||||
|
include ERB::Util
|
||||||
|
|
||||||
|
def initialize(builder, locals)
|
||||||
|
@builder = builder
|
||||||
|
locals.each do |key, value|
|
||||||
|
define_singleton_method(key) { value }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_partial(name, locals = {})
|
||||||
|
@builder.render_partial(name, locals)
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_relative_time(timestamp)
|
||||||
|
@builder.format_relative_time(timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_date(timestamp)
|
||||||
|
@builder.format_date(timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_binding
|
||||||
|
binding
|
||||||
|
end
|
||||||
|
end
|
||||||
|
DOCS_DIR = File.expand_path('../docs', __dir__)
|
||||||
|
DATA_DIR = File.join(DOCS_DIR, '_data')
|
||||||
|
OUTPUT_DIR = DOCS_DIR
|
||||||
|
THEME_SOURCE_DIR = File.expand_path('../theme', __dir__)
|
||||||
|
TEMPLATES_DIR = THEME_SOURCE_DIR
|
||||||
|
|
||||||
|
def self.build
|
||||||
|
new.generate_site
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_site
|
||||||
|
puts "🏗️ Building static site..."
|
||||||
|
|
||||||
|
setup_directories
|
||||||
|
load_data
|
||||||
|
generate_assets
|
||||||
|
generate_pages
|
||||||
|
|
||||||
|
puts "✅ Site built successfully in #{OUTPUT_DIR}"
|
||||||
|
puts "🌐 Open #{File.join(OUTPUT_DIR, 'index.html')} in your browser"
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_partial(name, locals = {})
|
||||||
|
partial_path = File.join(TEMPLATES_DIR, "_#{name}.html.erb")
|
||||||
|
unless File.exist?(partial_path)
|
||||||
|
raise ArgumentError, "Partial not found: #{partial_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
context = PartialContext.new(self, locals)
|
||||||
|
ERB.new(File.read(partial_path)).result(context.get_binding)
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_relative_time(timestamp)
|
||||||
|
return '' unless timestamp
|
||||||
|
|
||||||
|
begin
|
||||||
|
time = Time.parse(timestamp)
|
||||||
|
now = Time.now
|
||||||
|
diff = now - time
|
||||||
|
|
||||||
|
case diff
|
||||||
|
when 0..59
|
||||||
|
'just now'
|
||||||
|
when 60..3599
|
||||||
|
mins = (diff / 60).to_i
|
||||||
|
"#{mins} minute#{mins == 1 ? '' : 's'} ago"
|
||||||
|
when 3600..86399
|
||||||
|
hours = (diff / 3600).to_i
|
||||||
|
"#{hours} hour#{hours == 1 ? '' : 's'} ago"
|
||||||
|
when 86400..604799
|
||||||
|
days = (diff / 86400).to_i
|
||||||
|
"#{days} day#{days == 1 ? '' : 's'} ago"
|
||||||
|
when 604800..2419199
|
||||||
|
weeks = (diff / 604800).to_i
|
||||||
|
"#{weeks} week#{weeks == 1 ? '' : 's'} ago"
|
||||||
|
when 2419200..31535999
|
||||||
|
months = (diff / 2419200).to_i
|
||||||
|
"#{months} month#{months == 1 ? '' : 's'} ago"
|
||||||
|
else
|
||||||
|
years = (diff / 31536000).to_i
|
||||||
|
"#{years} year#{years == 1 ? '' : 's'} ago"
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_date(timestamp)
|
||||||
|
return '' unless timestamp
|
||||||
|
|
||||||
|
begin
|
||||||
|
Time.parse(timestamp).strftime('%b %d, %Y')
|
||||||
|
rescue
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def setup_directories
|
||||||
|
FileUtils.mkdir_p(File.join(OUTPUT_DIR, 'formula'))
|
||||||
|
unless templates_exist?
|
||||||
|
puts "⚠️ Templates not found in #{TEMPLATES_DIR}. Please ensure theme/*.html.erb files exist."
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_data
|
||||||
|
formulae_file = File.join(DATA_DIR, 'formulae.json')
|
||||||
|
@data = File.exist?(formulae_file) ? JSON.parse(File.read(formulae_file)) : default_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_assets
|
||||||
|
copy_assets
|
||||||
|
generate_css
|
||||||
|
minify_js
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_pages
|
||||||
|
generate_index_page
|
||||||
|
generate_formulae_pages
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_assets
|
||||||
|
assets_source_dir = File.join(THEME_SOURCE_DIR, 'assets')
|
||||||
|
assets_output_dir = File.join(OUTPUT_DIR, 'assets')
|
||||||
|
|
||||||
|
# Create the output assets directory if it doesn't exist
|
||||||
|
FileUtils.mkdir_p(assets_output_dir)
|
||||||
|
|
||||||
|
# Check if source assets directory exists
|
||||||
|
if Dir.exist?(assets_source_dir)
|
||||||
|
# Copy all files recursively, preserving directory structure
|
||||||
|
Dir.glob(File.join(assets_source_dir, '**', '*')).each do |source_file|
|
||||||
|
next if File.directory?(source_file)
|
||||||
|
|
||||||
|
# Calculate relative path from source assets dir
|
||||||
|
relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(assets_source_dir))
|
||||||
|
output_file = File.join(assets_output_dir, relative_path)
|
||||||
|
|
||||||
|
# Create parent directories if needed
|
||||||
|
FileUtils.mkdir_p(File.dirname(output_file))
|
||||||
|
|
||||||
|
# Copy the file
|
||||||
|
FileUtils.cp(source_file, output_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
asset_count = Dir.glob(File.join(assets_source_dir, '**', '*')).reject { |f| File.directory?(f) }.size
|
||||||
|
puts "📁 Copied #{asset_count} asset files to #{assets_output_dir}"
|
||||||
|
else
|
||||||
|
puts "⚠️ Assets source directory not found: #{assets_source_dir}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_css
|
||||||
|
css_source_path = File.join(THEME_SOURCE_DIR, 'style.css')
|
||||||
|
css_output_path = File.join(OUTPUT_DIR, 'style.css')
|
||||||
|
|
||||||
|
if File.exist?(css_source_path)
|
||||||
|
css_content = File.read(css_source_path)
|
||||||
|
minified_css = minify_css(css_content)
|
||||||
|
File.write(css_output_path, minified_css)
|
||||||
|
puts "📄 Generated and minified CSS (#{minified_css.length} bytes)"
|
||||||
|
else
|
||||||
|
puts "⚠️ CSS source file not found: #{css_source_path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def minify_css(css)
|
||||||
|
css
|
||||||
|
.gsub(/\/\*.*?\*\//m, '') # Remove comments
|
||||||
|
.gsub(/\s+/, ' ') # Collapse whitespace
|
||||||
|
.gsub(/\s*{\s*/, '{') # Remove spaces around braces
|
||||||
|
.gsub(/\s*}\s*/, '}')
|
||||||
|
.gsub(/\s*:\s*/, ':') # Remove spaces around colons
|
||||||
|
.gsub(/\s*;\s*/, ';') # Remove spaces around semicolons
|
||||||
|
.gsub(/\s*,\s*/, ',') # Remove spaces around commas
|
||||||
|
.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def minify_js
|
||||||
|
js_source_path = File.join(THEME_SOURCE_DIR, 'main.js')
|
||||||
|
js_output_path = File.join(OUTPUT_DIR, 'main.js')
|
||||||
|
|
||||||
|
if File.exist?(js_source_path)
|
||||||
|
js_content = File.read(js_source_path)
|
||||||
|
minified_js = minify_js_content(js_content)
|
||||||
|
File.write(js_output_path, minified_js)
|
||||||
|
puts "📄 Generated and minified JavaScript (#{minified_js.length} bytes)"
|
||||||
|
else
|
||||||
|
puts "⚠️ JavaScript source file not found: #{js_source_path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def minify_js_content(js)
|
||||||
|
# Simple minification that preserves string literals
|
||||||
|
# This is a basic approach that handles most cases
|
||||||
|
result = []
|
||||||
|
in_string = false
|
||||||
|
in_template = false
|
||||||
|
string_char = nil
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < js.length
|
||||||
|
char = js[i]
|
||||||
|
prev_char = i > 0 ? js[i - 1] : nil
|
||||||
|
|
||||||
|
# Handle string and template literal boundaries
|
||||||
|
if !in_string && !in_template && (char == '"' || char == "'" || char == '`')
|
||||||
|
in_string = true if char != '`'
|
||||||
|
in_template = true if char == '`'
|
||||||
|
string_char = char
|
||||||
|
result << char
|
||||||
|
elsif (in_string || in_template) && char == string_char && prev_char != '\\'
|
||||||
|
in_string = false
|
||||||
|
in_template = false
|
||||||
|
string_char = nil
|
||||||
|
result << char
|
||||||
|
elsif in_string || in_template
|
||||||
|
# Preserve everything inside strings and template literals
|
||||||
|
result << char
|
||||||
|
else
|
||||||
|
# Outside strings, apply minification
|
||||||
|
if char == '/' && i + 1 < js.length
|
||||||
|
next_char = js[i + 1]
|
||||||
|
if next_char == '/'
|
||||||
|
# Skip single-line comment
|
||||||
|
i += 1 while i < js.length && js[i] != "\n"
|
||||||
|
next
|
||||||
|
elsif next_char == '*'
|
||||||
|
# Skip multi-line comment
|
||||||
|
i += 2
|
||||||
|
while i < js.length - 1
|
||||||
|
break if js[i] == '*' && js[i + 1] == '/'
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
i += 1 # Skip the closing /
|
||||||
|
next
|
||||||
|
else
|
||||||
|
result << char
|
||||||
|
end
|
||||||
|
elsif char =~ /\s/
|
||||||
|
# Only add space if needed between identifiers
|
||||||
|
if result.last && result.last =~ /[a-zA-Z0-9_$]/ &&
|
||||||
|
i + 1 < js.length && js[i + 1] =~ /[a-zA-Z0-9_$]/
|
||||||
|
result << ' '
|
||||||
|
end
|
||||||
|
else
|
||||||
|
result << char
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
result.join.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_index_page
|
||||||
|
template = load_template('index.html.erb')
|
||||||
|
content = template.result(binding)
|
||||||
|
File.write(File.join(OUTPUT_DIR, 'index.html'), content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_formulae_pages
|
||||||
|
@data['formulae'].each do |formula|
|
||||||
|
generate_formula_page(formula)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate formulae index
|
||||||
|
template = load_template('formulae.html.erb')
|
||||||
|
content = template.result(binding)
|
||||||
|
File.write(File.join(OUTPUT_DIR, 'formulae.html'), content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_formula_page(formula)
|
||||||
|
@formula = formula
|
||||||
|
template = load_template('formula.html.erb')
|
||||||
|
content = template.result(binding)
|
||||||
|
|
||||||
|
formula_dir = File.join(OUTPUT_DIR, 'formula')
|
||||||
|
FileUtils.mkdir_p(formula_dir)
|
||||||
|
File.write(File.join(formula_dir, "#{formula['name']}.html"), content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_template(name)
|
||||||
|
template_path = File.join(TEMPLATES_DIR, name)
|
||||||
|
template_content = File.read(template_path)
|
||||||
|
ERB.new(template_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def templates_exist?
|
||||||
|
%w[index.html.erb formulae.html.erb formula.html.erb].all? do |template|
|
||||||
|
File.exist?(File.join(TEMPLATES_DIR, template))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_data
|
||||||
|
{
|
||||||
|
'tap_name' => 'ivuorinen/homebrew-tap',
|
||||||
|
'generated_at' => Time.now.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
||||||
|
'formulae_count' => 0,
|
||||||
|
'formulae' => []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allow running this script directly
|
||||||
|
if __FILE__ == $PROGRAM_NAME
|
||||||
|
SiteBuilder.build
|
||||||
|
end
|
||||||
121
scripts/make.rb
Normal file
121
scripts/make.rb
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
|
# Simple make-style command runner for homebrew tap
|
||||||
|
class Make
|
||||||
|
COMMANDS = {
|
||||||
|
'build' => 'Build the static site',
|
||||||
|
'serve' => 'Start development server',
|
||||||
|
'parse' => 'Parse formulae and generate JSON data',
|
||||||
|
'clean' => 'Clean generated files',
|
||||||
|
'help' => 'Show this help message'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def self.run(command = nil)
|
||||||
|
new.execute(command || ARGV[0])
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(command)
|
||||||
|
case command&.downcase
|
||||||
|
when 'build'
|
||||||
|
build
|
||||||
|
when 'serve'
|
||||||
|
serve
|
||||||
|
when 'parse'
|
||||||
|
parse
|
||||||
|
when 'clean'
|
||||||
|
clean
|
||||||
|
when 'help', nil
|
||||||
|
help
|
||||||
|
else
|
||||||
|
puts "❌ Unknown command: #{command}"
|
||||||
|
help
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build
|
||||||
|
puts "🏗️ Building homebrew tap documentation..."
|
||||||
|
|
||||||
|
success = system('ruby', script_path('parse_formulas.rb'))
|
||||||
|
exit 1 unless success
|
||||||
|
|
||||||
|
success = system('ruby', script_path('build_site.rb'))
|
||||||
|
exit 1 unless success
|
||||||
|
|
||||||
|
puts "✅ Build complete!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def serve
|
||||||
|
port = ARGV[1]&.to_i || 4000
|
||||||
|
host = ARGV[2] || 'localhost'
|
||||||
|
|
||||||
|
puts "🚀 Starting development server on http://#{host}:#{port}"
|
||||||
|
|
||||||
|
exec('ruby', script_path('serve.rb'), port.to_s, host)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse
|
||||||
|
puts "📋 Parsing formulae..."
|
||||||
|
|
||||||
|
success = system('ruby', script_path('parse_formulas.rb'))
|
||||||
|
exit 1 unless success
|
||||||
|
|
||||||
|
puts "✅ Formulae parsing complete!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def clean
|
||||||
|
puts "🧹 Cleaning generated files..."
|
||||||
|
|
||||||
|
files_to_clean = [
|
||||||
|
docs_path('index.html'),
|
||||||
|
docs_path('formulae.html'),
|
||||||
|
docs_path('formula'),
|
||||||
|
docs_path('_templates'),
|
||||||
|
docs_path('_data', 'formulae.json'),
|
||||||
|
docs_path('style.css'),
|
||||||
|
docs_path('main.js')
|
||||||
|
]
|
||||||
|
|
||||||
|
files_to_clean.each do |path|
|
||||||
|
if File.exist?(path)
|
||||||
|
FileUtils.rm_rf(path)
|
||||||
|
puts " 🗑️ Removed #{path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "✅ Clean complete!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def help
|
||||||
|
puts "Homebrew Tap Documentation Builder"
|
||||||
|
puts
|
||||||
|
puts "Usage: ruby scripts/make.rb <command>"
|
||||||
|
puts
|
||||||
|
puts "Commands:"
|
||||||
|
COMMANDS.each do |cmd, desc|
|
||||||
|
puts " #{cmd.ljust(10)} #{desc}"
|
||||||
|
end
|
||||||
|
puts
|
||||||
|
puts "Examples:"
|
||||||
|
puts " ruby scripts/make.rb build # Build the site"
|
||||||
|
puts " ruby scripts/make.rb serve # Start server on port 4000"
|
||||||
|
puts " ruby scripts/make.rb serve 3000 # Start server on port 3000"
|
||||||
|
puts " ruby scripts/make.rb serve 3000 0.0.0.0 # Start server on all interfaces"
|
||||||
|
end
|
||||||
|
|
||||||
|
def script_path(filename)
|
||||||
|
File.join(__dir__, filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
def docs_path(*parts)
|
||||||
|
File.join(__dir__, '..', 'docs', *parts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run if executed directly
|
||||||
|
Make.run if __FILE__ == $PROGRAM_NAME
|
||||||
@@ -114,7 +114,7 @@ class FormulaParser
|
|||||||
|
|
||||||
def write_json_output(formulae)
|
def write_json_output(formulae)
|
||||||
output = {
|
output = {
|
||||||
tap_name: 'ivuorinen/homebrew-tap',
|
tap_name: 'ivuorinen/tap',
|
||||||
generated_at: format_time_iso8601(Time.now),
|
generated_at: format_time_iso8601(Time.now),
|
||||||
formulae_count: formulae.length,
|
formulae_count: formulae.length,
|
||||||
formulae: formulae
|
formulae: formulae
|
||||||
|
|||||||
170
scripts/serve.rb
Normal file
170
scripts/serve.rb
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'webrick'
|
||||||
|
require 'fileutils'
|
||||||
|
require_relative 'parse_formulas'
|
||||||
|
require_relative 'build_site'
|
||||||
|
|
||||||
|
# Simple development server for the homebrew tap documentation
|
||||||
|
class DevServer
|
||||||
|
def self.serve(port: 4000, host: 'localhost')
|
||||||
|
new(port: port, host: host).start
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(port: 4000, host: 'localhost')
|
||||||
|
@port = port
|
||||||
|
@host = host
|
||||||
|
@site_dir = File.expand_path('../docs', __dir__)
|
||||||
|
@docs_dir = File.expand_path('../docs', __dir__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
puts "🔄 Building site..."
|
||||||
|
build_site
|
||||||
|
|
||||||
|
puts "🚀 Starting development server..."
|
||||||
|
puts "📍 Server address: http://#{@host}:#{@port}"
|
||||||
|
puts "📁 Serving from: #{@site_dir}"
|
||||||
|
puts "💡 Press Ctrl+C to stop"
|
||||||
|
|
||||||
|
start_server
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_site
|
||||||
|
# Generate formulae data
|
||||||
|
FormulaParser.run
|
||||||
|
|
||||||
|
# Build static site
|
||||||
|
SiteBuilder.build
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_server
|
||||||
|
server = WEBrick::HTTPServer.new(
|
||||||
|
Port: @port,
|
||||||
|
Host: @host,
|
||||||
|
DocumentRoot: @site_dir,
|
||||||
|
Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
|
||||||
|
AccessLog: [[
|
||||||
|
$stderr,
|
||||||
|
WEBrick::AccessLog::COMBINED_LOG_FORMAT
|
||||||
|
]]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle Ctrl+C gracefully
|
||||||
|
trap('INT') do
|
||||||
|
puts "\n👋 Stopping server..."
|
||||||
|
server.shutdown
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add custom mime types if needed
|
||||||
|
server.config[:MimeTypes]['json'] = 'application/json'
|
||||||
|
|
||||||
|
# Add auto-rebuild on file changes (simple polling)
|
||||||
|
start_file_watcher
|
||||||
|
|
||||||
|
server.start
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_file_watcher
|
||||||
|
Thread.new do
|
||||||
|
last_mtime = get_max_mtime
|
||||||
|
rebuild_pending = false
|
||||||
|
watched_files = get_watched_files_count
|
||||||
|
|
||||||
|
puts "👀 Watching #{watched_files} files for changes..."
|
||||||
|
|
||||||
|
loop do
|
||||||
|
sleep 1
|
||||||
|
current_mtime = get_max_mtime
|
||||||
|
|
||||||
|
if current_mtime > last_mtime && !rebuild_pending
|
||||||
|
rebuild_pending = true
|
||||||
|
changed_file = find_changed_file(last_mtime)
|
||||||
|
puts "📝 Changed: #{changed_file}" if changed_file
|
||||||
|
puts "🔄 Rebuilding in 1 second..."
|
||||||
|
|
||||||
|
# Debounce: wait for additional changes
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Check if more changes occurred during debounce period
|
||||||
|
final_mtime = get_max_mtime
|
||||||
|
|
||||||
|
puts "🔨 Building site..."
|
||||||
|
build_site
|
||||||
|
last_mtime = final_mtime
|
||||||
|
rebuild_pending = false
|
||||||
|
puts "✅ Rebuild complete"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_watched_files_count
|
||||||
|
files = get_all_watched_files
|
||||||
|
files.select { |f| File.exist?(f) && !File.directory?(f) }.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_changed_file(since_time)
|
||||||
|
files = get_all_watched_files
|
||||||
|
files.select { |f| File.exist?(f) && !File.directory?(f) }
|
||||||
|
.find { |f| File.mtime(f) > since_time }
|
||||||
|
&.sub(File.expand_path('..', __dir__) + '/', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_all_watched_files
|
||||||
|
[
|
||||||
|
# Watch Formula files for changes
|
||||||
|
Dir.glob(File.expand_path('../Formula/**/*.rb', __dir__)),
|
||||||
|
# Watch all theme files including partials
|
||||||
|
Dir.glob(File.expand_path('../theme/**/*', __dir__)),
|
||||||
|
# Specifically watch for erb templates and partials
|
||||||
|
Dir.glob(File.expand_path('../theme/*.erb', __dir__)),
|
||||||
|
Dir.glob(File.expand_path('../theme/_*.erb', __dir__)),
|
||||||
|
Dir.glob(File.expand_path('../theme/*.html.erb', __dir__)),
|
||||||
|
Dir.glob(File.expand_path('../theme/_*.html.erb', __dir__)),
|
||||||
|
# Watch CSS and JS
|
||||||
|
Dir.glob(File.expand_path('../theme/*.css', __dir__)),
|
||||||
|
Dir.glob(File.expand_path('../theme/*.js', __dir__)),
|
||||||
|
# Watch assets directory
|
||||||
|
Dir.glob(File.expand_path('../theme/assets/**/*', __dir__)),
|
||||||
|
# Watch build scripts for changes
|
||||||
|
[File.expand_path('../scripts/parse_formulas.rb', __dir__)],
|
||||||
|
[File.expand_path('../scripts/build_site.rb', __dir__)],
|
||||||
|
# Watch Makefile
|
||||||
|
[File.expand_path('../Makefile', __dir__)]
|
||||||
|
].flatten.compact.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_max_mtime
|
||||||
|
files_to_watch = get_all_watched_files
|
||||||
|
|
||||||
|
# Filter out non-existent files and directories, get modification times
|
||||||
|
existing_files = files_to_watch.select { |f| File.exist?(f) && !File.directory?(f) }
|
||||||
|
|
||||||
|
if existing_files.empty?
|
||||||
|
Time.at(0)
|
||||||
|
else
|
||||||
|
existing_files.map { |f| File.mtime(f) }.max
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Command line interface
|
||||||
|
if __FILE__ == $PROGRAM_NAME
|
||||||
|
# Check for --list-watched flag
|
||||||
|
if ARGV.include?('--list-watched')
|
||||||
|
server = DevServer.new
|
||||||
|
files = server.send(:get_all_watched_files).select { |f| File.exist?(f) && !File.directory?(f) }
|
||||||
|
puts "📋 Watching #{files.count} files:"
|
||||||
|
files.sort.each { |f| puts " - #{f.sub(File.expand_path('..', __dir__) + '/', '')}" }
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
port = ARGV[0]&.to_i || 4000
|
||||||
|
host = ARGV[1] || 'localhost'
|
||||||
|
|
||||||
|
DevServer.serve(port: port, host: host)
|
||||||
|
end
|
||||||
10
theme/_command_input.html.erb
Normal file
10
theme/_command_input.html.erb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="install-cmd relative flex items-center gap-<%= size == 'sm' ? '2' : '4' %> rounded-2xl border border-slate-200 bg-slate-900 shadow-sm dark:border-slate-700 dark:bg-slate-950">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
value="<%= h(command) %>"
|
||||||
|
onclick="this.select()"
|
||||||
|
class="flex-1 bg-transparent <%= size == 'sm' ? 'py-2 text-xs' : 'py-3 text-sm' %> font-mono text-slate-100 outline-none cursor-pointer border-0 appearance-none focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-400 dark:focus-visible:outline-sky-300 focus-visible:outline-offset-[-2px] rounded-2xl px-8"
|
||||||
|
>
|
||||||
|
<span class="<%= size == 'sm' ? 'text-[10px]' : 'text-xs' %> uppercase tracking-wide text-slate-400 pr-8"><%= h(label || 'CLI') %></span>
|
||||||
|
</div>
|
||||||
7
theme/_footer.html.erb
Normal file
7
theme/_footer.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<footer class="border-t border-slate-200 pt-6 text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
||||||
|
<p class="px-8">
|
||||||
|
<time datetime="<%= h(generated_at) %>" title="<%= h(generated_at) %>">
|
||||||
|
Generated at <%= h(format_relative_time(generated_at)) %>
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
55
theme/_formula_card.html.erb
Normal file
55
theme/_formula_card.html.erb
Normal file
@@ -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
|
||||||
|
%>
|
||||||
|
<article class="formula-card flex h-full flex-col gap-4 rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm ring-1 ring-black/5 transition hover:-translate-y-1 hover:shadow-md dark:border-slate-700 dark:bg-slate-900/70 dark:ring-white/10">
|
||||||
|
<header class="space-y-2">
|
||||||
|
<h3 class="text-xl font-semibold">
|
||||||
|
<a href="<%= base_path %>formula/<%= h(formula_slug) %>.html" class="transition hover:text-sky-600 dark:hover:text-sky-400"><%= h(formula_name) %></a>
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-300"><%= h(formula_description) %></p>
|
||||||
|
<% if show_homepage && formula['homepage'] %>
|
||||||
|
<p class="text-sm">
|
||||||
|
<a href="<%= h(formula['homepage'].to_s) %>" target="_blank" class="inline-flex items-center gap-1 text-sky-600 transition hover:text-sky-500 dark:text-sky-400 dark:hover:text-sky-300">
|
||||||
|
Homepage
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<% if show_last_modified && formula['last_modified'] %>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
<span>Updated <%= format_relative_time(formula['last_modified']) %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<% if formula['license'] %>
|
||||||
|
<span class="badge inline-flex items-center rounded-full bg-sky-500 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-sky-400">License: <%= h(formula['license'].to_s) %></span>
|
||||||
|
<% end %>
|
||||||
|
<% if formula['version'] %>
|
||||||
|
<span class="badge inline-flex items-center rounded-full bg-emerald-500 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-emerald-400">v<%= h(formula['version'].to_s) %></span>
|
||||||
|
<% end %>
|
||||||
|
<% if show_dependencies %>
|
||||||
|
<% Array(formula['dependencies']).compact.each do |dep| %>
|
||||||
|
<span class="badge inline-flex items-center rounded-full bg-slate-800 px-3 py-1 text-xs uppercase tracking-wide text-white dark:bg-slate-700">dep: <%= h(dep.to_s) %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
31
theme/_head.html.erb
Normal file
31
theme/_head.html.erb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<% page_title = title.to_s %>
|
||||||
|
<title><%= h(page_title) %></title>
|
||||||
|
<% stylesheet = style_path || 'style.css' %>
|
||||||
|
<link rel="stylesheet" href="<%= h(stylesheet) %>">
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
const stored = window.localStorage.getItem("theme");
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
const theme = stored || (prefersDark ? "dark" : "light");
|
||||||
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||||
|
document.documentElement.classList.toggle("light", theme !== "dark");
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
} catch (error) {
|
||||||
|
document.documentElement.classList.toggle("dark", false);
|
||||||
|
document.documentElement.classList.toggle("light", true);
|
||||||
|
document.documentElement.setAttribute("data-theme", "light");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
15
theme/_header.html.erb
Normal file
15
theme/_header.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<header class="flex flex-col gap-6">
|
||||||
|
<nav aria-label="Main navigation" class="px-8">
|
||||||
|
<%= render_partial('nav', active: active_page, base_path: base_path) %>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="header flex flex-col gap-6 rounded-3xl border border-slate-200 bg-white/80 p-8 shadow-sm ring-1 ring-black/5 backdrop-blur dark:border-slate-700 dark:bg-slate-900/80 dark:ring-white/10">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight md:text-4xl"><%= h(tap_name) %></h1>
|
||||||
|
<p class="max-w-3xl text-base text-slate-600 dark:text-slate-300"><%= h(description) %></p>
|
||||||
|
</div>
|
||||||
|
<button class="theme-toggle inline-flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full border border-slate-300 bg-white/80 text-xl text-slate-600 shadow-sm transition hover:bg-slate-100 hover:text-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500 dark:border-slate-600 dark:bg-slate-900/70 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white" aria-label="Toggle dark mode" tabindex="0">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
13
theme/_nav.html.erb
Normal file
13
theme/_nav.html.erb
Normal file
@@ -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 %>
|
||||||
|
<nav aria-label="Main navigation" class="flex flex-wrap items-center gap-3 text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||||
|
<% home_classes = [nav_base, (active_tab == :home ? nav_active : nil)].compact.join(' ') %>
|
||||||
|
<% formulae_classes = [nav_base, (active_tab == :formulae ? nav_active : nil)].compact.join(' ') %>
|
||||||
|
<a href="<%= h(href.call('index.html')) %>" class="<%= home_classes %>">Home</a>
|
||||||
|
<a href="<%= h(href.call('formulae.html')) %>" class="<%= formulae_classes %>">All Formulae</a>
|
||||||
|
</nav>
|
||||||
6
theme/_nothing_here.html.erb
Normal file
6
theme/_nothing_here.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<main class="flex flex-1 flex-col gap-8">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white/80 p-8 text-center shadow-sm ring-1 ring-black/5 backdrop-blur dark:border-slate-700 dark:bg-slate-900/80 dark:ring-white/10">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight mb-4">No formulae available</h2>
|
||||||
|
<p class="text-slate-600 dark:text-slate-300">This tap currently has no formulae. Please check back later.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
BIN
theme/assets/MonaspaceArgonVar.woff
Normal file
BIN
theme/assets/MonaspaceArgonVar.woff
Normal file
Binary file not shown.
BIN
theme/assets/MonaspaceArgonVar.woff2
Normal file
BIN
theme/assets/MonaspaceArgonVar.woff2
Normal file
Binary file not shown.
111
theme/formula.html.erb
Normal file
111
theme/formula.html.erb
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full min-h-full scroll-smooth font-sans transition-colors duration-200">
|
||||||
|
<% 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') %>
|
||||||
|
<body class="min-h-screen bg-slate-50 text-slate-900 transition-colors duration-200 dark:bg-slate-950 dark:text-slate-100">
|
||||||
|
<div class="mx-auto flex min-h-screen max-w-6xl flex-col gap-12 px-6 py-10">
|
||||||
|
<%= render_partial('header', tap_name: name, description: @formula['description'].to_s, active_page: :formulae, base_path: '../') %>
|
||||||
|
|
||||||
|
<main class="flex flex-1 flex-col gap-8">
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between px-8 pr-6">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight h-10">Installation</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<% if @formula['url'] %>
|
||||||
|
<a href="<%= h(@formula['url'].to_s) %>" target="_blank" class="inline-flex items-center justify-center rounded-full border border-slate-200 p-2 text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 dark:border-slate-700 dark:text-slate-400 dark:hover:border-slate-600 dark:hover:bg-slate-800 dark:hover:text-white" aria-label="Download source">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% if @formula['homepage'] %>
|
||||||
|
<a href="<%= h(@formula['homepage'].to_s) %>" target="_blank" class="inline-flex items-center justify-center rounded-full border border-slate-200 p-2 text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 dark:border-slate-700 dark:text-slate-400 dark:hover:border-slate-600 dark:hover:bg-slate-800 dark:hover:text-white" aria-label="View on GitHub">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render_partial('command_input', command: "brew install #{name}", size: 'lg', label: 'CLI') %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white/80 shadow-sm ring-1 ring-black/5 dark:border-slate-700 dark:bg-slate-900/70 dark:ring-white/10">
|
||||||
|
<table class="w-full divide-y divide-slate-200 text-sm dark:divide-slate-700">
|
||||||
|
<caption class="sr-only">Formula Details</caption>
|
||||||
|
<tbody>
|
||||||
|
<% if @formula['version'] %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">Version</th>
|
||||||
|
<td class="px-4 py-3 text-slate-600 dark:text-slate-200"><%= h(@formula['version'].to_s) %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if @formula['license'] %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">License</th>
|
||||||
|
<td class="px-4 py-3 text-slate-600 dark:text-slate-200"><%= h(@formula['license'].to_s) %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if @formula['homepage'] %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">Homepage</th>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<a href="<%= h(@formula['homepage'].to_s) %>" target="_blank" class="inline-flex items-center gap-1 text-sky-600 transition hover:text-sky-500 dark:text-sky-400 dark:hover:text-sky-300">
|
||||||
|
<%= h(@formula['homepage'].to_s) %>
|
||||||
|
<span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% dependencies = Array(@formula['dependencies']).reject(&:nil?) %>
|
||||||
|
<% if dependencies.any? %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">Dependencies</th>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<% dependencies.each do |dep| %>
|
||||||
|
<span class="inline-flex items-center rounded-full bg-slate-800 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-slate-700">dep: <%= h(dep.to_s) %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">Formula File</th>
|
||||||
|
<td class="px-4 py-3"><code class="rounded-xl bg-slate-900 px-3 py-2 font-mono text-xs text-slate-100 dark:bg-slate-950"><%= h(@formula['file_path'].to_s) %></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">Last Modified</th>
|
||||||
|
<td class="px-4 py-3 text-slate-600 dark:text-slate-200"><%= h(@formula['last_modified'].to_s) %></td>
|
||||||
|
</tr>
|
||||||
|
<% if @formula['sha256'] %>
|
||||||
|
<tr class="divide-x divide-slate-200 dark:divide-slate-700">
|
||||||
|
<th scope="row" class="w-40 bg-slate-50 px-4 py-3 text-left font-medium text-slate-700 dark:bg-slate-900 dark:text-slate-300">SHA256</th>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
value="<%= h(@formula['sha256'].to_s) %>"
|
||||||
|
onclick="this.select()"
|
||||||
|
class="w-full bg-slate-50 dark:bg-slate-800 px-3 py-1.5 font-mono text-xs text-slate-800 dark:text-slate-200 rounded-lg border border-slate-200 dark:border-slate-600 cursor-pointer appearance-none focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-400 dark:focus-visible:outline-sky-300 focus-visible:outline-offset-[-2px]"
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= render_partial('footer', generated_at: @data['generated_at']) %>
|
||||||
|
<script src="../main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
theme/formulae.html.erb
Normal file
46
theme/formulae.html.erb
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full min-h-full scroll-smooth font-sans transition-colors duration-200">
|
||||||
|
<% 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') %>
|
||||||
|
<body class="min-h-screen bg-slate-50 text-slate-900 transition-colors duration-200 dark:bg-slate-950 dark:text-slate-100">
|
||||||
|
<div class="mx-auto flex min-h-screen max-w-6xl flex-col gap-12 px-6 py-10">
|
||||||
|
<%= 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 %>
|
||||||
|
|
||||||
|
<main class="flex flex-1 flex-col gap-8">
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="flex items-end justify-between gap-4">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight px-8">All Formulae</h2>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 px-8"><%= h(@data['formulae_count'].to_s) %> total</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="formula-search"
|
||||||
|
placeholder="Search formulae..."
|
||||||
|
class="w-full rounded-xl border border-slate-200 bg-white px-8 py-2 text-sm placeholder-slate-400 shadow-sm transition focus:border-sky-400 focus:outline-none focus:ring-1 focus:ring-sky-400 dark:border-slate-700 dark:bg-slate-900 dark:placeholder-slate-500 dark:focus:border-sky-300 dark:focus:ring-sky-300"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formula-grid grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<%
|
||||||
|
# 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 %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= render_partial('footer', generated_at: @data['generated_at']) %>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
49
theme/index.html.erb
Normal file
49
theme/index.html.erb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full min-h-full scroll-smooth font-sans transition-colors duration-200">
|
||||||
|
<% 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') %>
|
||||||
|
<body class="min-h-screen bg-slate-50 text-slate-900 transition-colors duration-200 dark:bg-slate-950 dark:text-slate-100">
|
||||||
|
<div class="mx-auto flex min-h-screen max-w-6xl flex-col gap-12 px-6 py-10">
|
||||||
|
<%= 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 %>
|
||||||
|
|
||||||
|
<main class="flex flex-1 flex-col gap-8">
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight px-8 h-10">Quick Start</h2>
|
||||||
|
<%= render_partial('command_input', command: "brew tap #{safe_tap_name}", size: 'lg', label: 'CLI') %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="flex items-end justify-between gap-4">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight px-8">Latest Updates</h2>
|
||||||
|
<a href="formulae.html" class="text-sm text-sky-600 hover:text-sky-500 dark:text-sky-400 dark:hover:text-sky-300 px-8">View all <%= h(@data['formulae_count'].to_s) %> formulae →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formula-grid grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<%
|
||||||
|
# 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 %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<%= render_partial('footer', generated_at: @data['generated_at'].to_s) %>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
156
theme/main.js
Normal file
156
theme/main.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
43
theme/style.css
Normal file
43
theme/style.css
Normal file
@@ -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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user