feat: full site

This commit is contained in:
2025-09-21 23:31:15 +03:00
parent 6de65bca11
commit c63ef78b03
38 changed files with 1541 additions and 1481 deletions

View File

@@ -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.*"

View File

@@ -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

16
.gitignore vendored
View File

@@ -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

View File

@@ -1 +1 @@
3.4.5
3.3.6

View 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
View 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
View File

@@ -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.

0
docs/.gitkeep Normal file
View File

View File

@@ -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]

View File

@@ -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
View File

View 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>&copy; {{ 'now' | date: '%Y' }} {{ site.title }}. Built with Jekyll and GitHub Pages.</p>
</div>
</footer>
</body>
</html>

View File

@@ -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
View File

View 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
View File

View 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 %}

View File

@@ -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
View 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
View 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

View File

@@ -114,7 +114,7 @@ class FormulaParser
def write_json_output(formulae)
output = {
tap_name: 'ivuorinen/homebrew-tap',
tap_name: 'ivuorinen/tap',
generated_at: format_time_iso8601(Time.now),
formulae_count: formulae.length,
formulae: formulae

170
scripts/serve.rb Normal file
View 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

View 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
View 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>

View 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
View 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
View 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
View 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>

View 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>

Binary file not shown.

Binary file not shown.

111
theme/formula.html.erb Normal file
View 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
View 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
View 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
View 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
View 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";
}