mirror of
https://github.com/ivuorinen/homebrew-tap.git
synced 2026-01-26 03:14:04 +00:00
chore: fixes, several improvements and refactorings (#2)
* chore: fixes * chore: rubocop fixes, linting, etc. * chore: switching to use `brew style` only * chore: use `brew style` for linting, skip example formulae in ci.yml * chore(lint): fixes, additions and tweaks
This commit is contained in:
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -49,8 +49,16 @@ jobs:
|
|||||||
- name: Run brew test-bot (tap syntax)
|
- name: Run brew test-bot (tap syntax)
|
||||||
run: brew test-bot --only-tap-syntax
|
run: brew test-bot --only-tap-syntax
|
||||||
|
|
||||||
|
- name: Check for real formulae (non-examples)
|
||||||
|
id: check-formulae
|
||||||
|
run: |
|
||||||
|
# Count formulae that are not example formulae
|
||||||
|
REAL_FORMULAE=$(find Formula -name "*.rb" ! -name "example-*.rb" | wc -l | tr -d ' ')
|
||||||
|
echo "real_formulae_count=$REAL_FORMULAE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Found $REAL_FORMULAE real formulae (excluding examples)"
|
||||||
|
|
||||||
- name: Run brew test-bot (formulae)
|
- name: Run brew test-bot (formulae)
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request' && steps.check-formulae.outputs.real_formulae_count > 0
|
||||||
run: brew test-bot --only-formulae
|
run: brew test-bot --only-formulae
|
||||||
|
|
||||||
- name: Upload bottles as artifact
|
- name: Upload bottles as artifact
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
.bundle/
|
.bundle/
|
||||||
vendor/bundle/
|
vendor/bundle/
|
||||||
|
.ruby-lsp/
|
||||||
|
|
||||||
# Generated site files
|
# Generated site files
|
||||||
docs/**/*.html
|
docs/**/*.html
|
||||||
@@ -27,3 +28,7 @@ docs/assets/*
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
.rubocop_todo.yml
|
||||||
|
.serena
|
||||||
|
CLAUDE.md
|
||||||
|
node_modules/
|
||||||
|
|||||||
29
.pre-commit-config.yaml
Normal file
29
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-json
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: detect-private-key
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: brew-style
|
||||||
|
name: Brew Style
|
||||||
|
entry: brew style --fix --reset-cache .
|
||||||
|
language: system
|
||||||
|
types: [ruby]
|
||||||
|
pass_filenames: false
|
||||||
|
require_serial: true
|
||||||
|
exclude: ^(docs/|vendor/|bin/|tmp/|\.git/|node_modules/)
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or: [css, javascript, json, yaml]
|
||||||
|
exclude: "^docs/"
|
||||||
41
.rubocop.yml
41
.rubocop.yml
@@ -1,41 +0,0 @@
|
|||||||
AllCops:
|
|
||||||
TargetRubyVersion: 3.4
|
|
||||||
NewCops: enable
|
|
||||||
Exclude:
|
|
||||||
- 'vendor/**/*'
|
|
||||||
- 'docs/**/*'
|
|
||||||
- '.bundle/**/*'
|
|
||||||
|
|
||||||
Layout/LineLength:
|
|
||||||
Max: 120
|
|
||||||
AllowedPatterns:
|
|
||||||
- '^\s*#'
|
|
||||||
- 'url "'
|
|
||||||
|
|
||||||
Layout/IndentationStyle:
|
|
||||||
EnforcedStyle: spaces
|
|
||||||
|
|
||||||
Layout/IndentationWidth:
|
|
||||||
Width: 2
|
|
||||||
|
|
||||||
Style/StringLiterals:
|
|
||||||
EnforcedStyle: double_quotes
|
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
|
||||||
Enabled: true
|
|
||||||
EnforcedStyle: always
|
|
||||||
|
|
||||||
Naming/FileName:
|
|
||||||
Exclude:
|
|
||||||
- 'Formula/**/*.rb'
|
|
||||||
|
|
||||||
Metrics/MethodLength:
|
|
||||||
Max: 30
|
|
||||||
|
|
||||||
Metrics/ClassLength:
|
|
||||||
Max: 150
|
|
||||||
|
|
||||||
Metrics/BlockLength:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/**/*'
|
|
||||||
- '*.gemspec'
|
|
||||||
@@ -1 +1 @@
|
|||||||
3.3.6
|
3.4.5
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Homebrew formula for ExampleTool - a demonstration tool for this tap
|
||||||
class ExampleTool < Formula
|
class ExampleTool < Formula
|
||||||
desc "An example tool to demonstrate the tap functionality"
|
desc "Imaginery tool to demonstrate the tap functionality"
|
||||||
homepage "https://github.com/ivuorinen/example-tool"
|
homepage "https://github.com/ivuorinen/example-tool"
|
||||||
url "https://github.com/ivuorinen/example-tool/archive/v1.0.0.tar.gz"
|
url "https://github.com/ivuorinen/example-tool/refs/tags/v1.0.0.tar.gz"
|
||||||
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
license "MIT"
|
license "MIT"
|
||||||
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Homebrew formula for ExampleTool2 - a second demonstration tool for this tap
|
||||||
class ExampleTool2 < Formula
|
class ExampleTool2 < Formula
|
||||||
desc "A second example tool to demonstrate the tap functionality"
|
desc "Second example tool to demonstrate the tap functionality"
|
||||||
homepage "https://github.com/ivuorinen/example-tool2"
|
homepage "https://github.com/ivuorinen/example-tool2"
|
||||||
url "https://github.com/ivuorinen/example-tool2/archive/v2.0.0.tar.gz"
|
url "https://github.com/ivuorinen/example-tool2/refs/tags/v2.0.0.tar.gz"
|
||||||
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
license "MIT"
|
license "MIT"
|
||||||
|
|
||||||
10
Gemfile
10
Gemfile
@@ -1,13 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
ruby "3.4.5"
|
ruby "3.4.5"
|
||||||
|
|
||||||
gem "parser", "~> 3.3"
|
|
||||||
gem "json", "~> 2.7"
|
gem "json", "~> 2.7"
|
||||||
|
|
||||||
group :development, :test do
|
|
||||||
gem "rubocop", "~> 1.69"
|
|
||||||
gem "rubocop-rspec", "~> 3.6"
|
|
||||||
gem "rubocop-performance", "~> 1.25"
|
|
||||||
gem "rspec", "~> 3.13"
|
|
||||||
end
|
|
||||||
|
|||||||
35
Makefile
35
Makefile
@@ -1,7 +1,7 @@
|
|||||||
# Homebrew Tap Makefile
|
# Homebrew Tap Makefile
|
||||||
# Provides convenient commands for building and managing the tap documentation
|
# Provides convenient commands for building and managing the tap documentation
|
||||||
|
|
||||||
.PHONY: help build serve parse clean test install dev setup check
|
.PHONY: help build serve parse clean test install dev setup check lint update
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@@ -87,6 +87,35 @@ install: ## Install development dependencies (if Gemfile exists)
|
|||||||
echo "ℹ️ No Gemfile found, skipping dependency installation"; \
|
echo "ℹ️ No Gemfile found, skipping dependency installation"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
update: ## Update all gems to their latest versions (respecting Gemfile constraints)
|
||||||
|
@if [ -f Gemfile ]; then \
|
||||||
|
echo "🔄 Updating Ruby dependencies..."; \
|
||||||
|
echo "📦 Running bundle update..."; \
|
||||||
|
bundle update; \
|
||||||
|
echo "✅ All gems updated to latest versions!"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "💡 Current gem versions:"; \
|
||||||
|
bundle list | grep -E "^\s*\*" | head -10; \
|
||||||
|
else \
|
||||||
|
echo "❌ No Gemfile found"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-bundler: ## Update bundler itself to the latest version
|
||||||
|
@echo "🔄 Updating Bundler..."
|
||||||
|
@gem update bundler
|
||||||
|
@echo "✅ Bundler updated!"
|
||||||
|
@echo "Current version: $$(bundle --version)"
|
||||||
|
|
||||||
|
outdated: ## Show outdated gems
|
||||||
|
@if [ -f Gemfile ]; then \
|
||||||
|
echo "📋 Checking for outdated gems..."; \
|
||||||
|
bundle outdated || true; \
|
||||||
|
else \
|
||||||
|
echo "❌ No Gemfile found"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
watch: ## Watch for file changes and auto-rebuild (alias for serve)
|
watch: ## Watch for file changes and auto-rebuild (alias for serve)
|
||||||
@$(MAKE) serve
|
@$(MAKE) serve
|
||||||
|
|
||||||
@@ -116,6 +145,10 @@ tap-install: ## Install this tap locally for testing
|
|||||||
@echo "🍺 Installing tap locally..."
|
@echo "🍺 Installing tap locally..."
|
||||||
@brew tap $$(pwd)
|
@brew tap $$(pwd)
|
||||||
|
|
||||||
|
lint: ## Lint all Ruby scripts
|
||||||
|
@echo "🔍 Linting Ruby scripts..."
|
||||||
|
@brew style --fix --reset-cache .
|
||||||
|
|
||||||
formula-new: ## Create a new formula template (usage: make formula-new NAME=myformula)
|
formula-new: ## Create a new formula template (usage: make formula-new NAME=myformula)
|
||||||
@if [ -z "$(NAME)" ]; then \
|
@if [ -z "$(NAME)" ]; then \
|
||||||
echo "❌ Please provide a formula name: make formula-new NAME=myformula"; \
|
echo "❌ Please provide a formula name: make formula-new NAME=myformula"; \
|
||||||
|
|||||||
546
scripts/build_site.rb
Normal file → Executable file
546
scripts/build_site.rb
Normal file → Executable file
@@ -1,15 +1,311 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
# typed: strict
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'json'
|
require "json"
|
||||||
require 'fileutils'
|
require "fileutils"
|
||||||
require 'erb'
|
require "erb"
|
||||||
require 'pathname'
|
require "pathname"
|
||||||
require 'time'
|
require "time"
|
||||||
|
|
||||||
|
# Simple polyfill for Homebrew extensions
|
||||||
|
class Array
|
||||||
|
def exclude?(item)
|
||||||
|
!include?(item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Simple static site generator for homebrew tap documentation
|
# Simple static site generator for homebrew tap documentation
|
||||||
|
# Module for formatting timestamps and dates
|
||||||
|
module TimeFormatter
|
||||||
|
SECONDS_PER_MINUTE = 60
|
||||||
|
SECONDS_PER_HOUR = 3600
|
||||||
|
SECONDS_PER_DAY = 86_400
|
||||||
|
SECONDS_PER_WEEK = 604_800
|
||||||
|
SECONDS_PER_MONTH = 2_419_200
|
||||||
|
SECONDS_PER_YEAR = 31_536_000
|
||||||
|
|
||||||
|
def format_relative_time(timestamp)
|
||||||
|
return "" unless timestamp
|
||||||
|
|
||||||
|
begin
|
||||||
|
diff = calculate_time_difference(timestamp)
|
||||||
|
return "just now" if diff < SECONDS_PER_MINUTE
|
||||||
|
|
||||||
|
format_time_by_category(diff)
|
||||||
|
rescue
|
||||||
|
""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_date(timestamp)
|
||||||
|
return "" unless timestamp
|
||||||
|
|
||||||
|
begin
|
||||||
|
Time.parse(timestamp).strftime("%b %d, %Y")
|
||||||
|
rescue
|
||||||
|
""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def calculate_time_difference(timestamp)
|
||||||
|
time = Time.parse(timestamp)
|
||||||
|
Time.now - time
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_time_by_category(diff)
|
||||||
|
case diff
|
||||||
|
when SECONDS_PER_MINUTE...SECONDS_PER_HOUR
|
||||||
|
format_time_unit(diff / SECONDS_PER_MINUTE, "minute")
|
||||||
|
when SECONDS_PER_HOUR...SECONDS_PER_DAY
|
||||||
|
format_time_unit(diff / SECONDS_PER_HOUR, "hour")
|
||||||
|
when SECONDS_PER_DAY...SECONDS_PER_WEEK
|
||||||
|
format_time_unit(diff / SECONDS_PER_DAY, "day")
|
||||||
|
when SECONDS_PER_WEEK...SECONDS_PER_MONTH
|
||||||
|
format_time_unit(diff / SECONDS_PER_WEEK, "week")
|
||||||
|
when SECONDS_PER_MONTH...SECONDS_PER_YEAR
|
||||||
|
format_time_unit(diff / SECONDS_PER_MONTH, "month")
|
||||||
|
else
|
||||||
|
format_time_unit(diff / SECONDS_PER_YEAR, "year")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_time_unit(value, unit)
|
||||||
|
count = value.to_i
|
||||||
|
"#{count} #{unit}#{"s" if count != 1} ago"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Module for processing and copying assets
|
||||||
|
module AssetProcessor
|
||||||
|
DOCS_DIR = File.expand_path("../docs", __dir__).freeze
|
||||||
|
OUTPUT_DIR = DOCS_DIR
|
||||||
|
THEME_SOURCE_DIR = File.expand_path("../theme", __dir__).freeze
|
||||||
|
|
||||||
|
def copy_assets
|
||||||
|
copy_asset_files
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_css
|
||||||
|
css_path = File.join(THEME_SOURCE_DIR, "style.css")
|
||||||
|
output_path = File.join(OUTPUT_DIR, "style.css")
|
||||||
|
|
||||||
|
return unless File.exist?(css_path)
|
||||||
|
|
||||||
|
css_content = File.read(css_path)
|
||||||
|
minified_css = minify_css(css_content)
|
||||||
|
File.write(output_path, minified_css)
|
||||||
|
puts "📄 Generated CSS file: #{output_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def minify_js
|
||||||
|
js_path = File.join(THEME_SOURCE_DIR, "main.js")
|
||||||
|
output_path = File.join(OUTPUT_DIR, "main.js")
|
||||||
|
|
||||||
|
return unless File.exist?(js_path)
|
||||||
|
|
||||||
|
js_content = File.read(js_path)
|
||||||
|
minified_js = JavaScriptMinifier.minify(js_content)
|
||||||
|
File.write(output_path, minified_js)
|
||||||
|
puts "🔧 Generated JS file: #{output_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def copy_asset_files
|
||||||
|
assets_source_dir = File.join(THEME_SOURCE_DIR, "assets")
|
||||||
|
assets_output_dir = File.join(OUTPUT_DIR, "assets")
|
||||||
|
|
||||||
|
FileUtils.mkdir_p(assets_output_dir)
|
||||||
|
|
||||||
|
return handle_missing_assets(assets_source_dir) unless Dir.exist?(assets_source_dir)
|
||||||
|
|
||||||
|
copy_files_recursively(assets_source_dir, assets_output_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_missing_assets(assets_source_dir)
|
||||||
|
puts "⚠️ Assets source directory not found: #{assets_source_dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_files_recursively(source_dir, output_dir)
|
||||||
|
asset_files = Dir.glob(File.join(source_dir, "**", "*")).reject { |f| File.directory?(f) }
|
||||||
|
|
||||||
|
asset_files.each do |source_file|
|
||||||
|
copy_single_asset(source_file, source_dir, output_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "📁 Copied #{asset_files.count} asset files to #{output_dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_single_asset(source_file, source_dir, output_dir)
|
||||||
|
relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
|
||||||
|
output_file = File.join(output_dir, relative_path)
|
||||||
|
|
||||||
|
FileUtils.mkdir_p(File.dirname(output_file))
|
||||||
|
FileUtils.cp(source_file, output_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def minify_css(css)
|
||||||
|
css.gsub(%r{/\*.*?\*/}m, "")
|
||||||
|
.gsub(/\s+/, " ")
|
||||||
|
.gsub(/;\s*}/, "}")
|
||||||
|
.strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper module for JavaScript character handling
|
||||||
|
module JavaScriptCharacters
|
||||||
|
NEWLINE_CHARS = ["\n", "\r"].freeze
|
||||||
|
WHITESPACE_CHARS = [" ", "\t"].freeze
|
||||||
|
SPECIAL_CHARS = [";", "{", "}", "(", ")", ",", ":", "=", "+", "-", "*", "/", "%", "!", "&", "|", "^", "~", "<",
|
||||||
|
">", "?"].freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper module for JavaScript comment and string processing
|
||||||
|
module JavaScriptProcessor
|
||||||
|
include JavaScriptCharacters
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def skip_line_comment(index)
|
||||||
|
index += 1 while index < javascript.length && javascript[index] != "\n"
|
||||||
|
index
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_block_comment(index)
|
||||||
|
index += 2
|
||||||
|
while index < javascript.length - 1
|
||||||
|
break if javascript[index] == "*" && javascript[index + 1] == "/"
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
end
|
||||||
|
index + 2
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_string_content(result, quote_char, index)
|
||||||
|
while index < javascript.length && javascript[index] != quote_char
|
||||||
|
result += javascript[index]
|
||||||
|
index += 1 if javascript[index] == "\\"
|
||||||
|
index += 1
|
||||||
|
end
|
||||||
|
index
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_closing_quote(result, index)
|
||||||
|
result << javascript[index] if index < javascript.length
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_to_next_line(index)
|
||||||
|
index += 1 while index < javascript.length && NEWLINE_CHARS.include?(javascript[index])
|
||||||
|
index
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_whitespace(index)
|
||||||
|
index += 1 while index < javascript.length && WHITESPACE_CHARS.include?(javascript[index])
|
||||||
|
index
|
||||||
|
end
|
||||||
|
|
||||||
|
def preserve_space?(result)
|
||||||
|
return false if result.empty?
|
||||||
|
|
||||||
|
last_char = result[-1]
|
||||||
|
[";", "{", "}", "(", ")", ",", ":", "=", "+", "-", "*", "/", "%", "!", "&", "|", "^", "~", "<", ">",
|
||||||
|
"?"].exclude?(last_char)
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :javascript
|
||||||
|
end
|
||||||
|
|
||||||
|
# Class for JavaScript minification
|
||||||
|
class JavaScriptMinifier
|
||||||
|
include JavaScriptProcessor
|
||||||
|
|
||||||
|
def self.minify(javascript)
|
||||||
|
new(javascript).minify
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(javascript)
|
||||||
|
@javascript = javascript
|
||||||
|
end
|
||||||
|
|
||||||
|
def minify
|
||||||
|
remove_comments_and_whitespace
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_comments_and_whitespace
|
||||||
|
result = ""
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < javascript.length
|
||||||
|
char = javascript[i]
|
||||||
|
|
||||||
|
case char
|
||||||
|
when "/"
|
||||||
|
i = handle_slash(result, i)
|
||||||
|
when '"', "'"
|
||||||
|
i = handle_string_literal(result, char, i)
|
||||||
|
when "\n", "\r"
|
||||||
|
i = handle_newline(result, i)
|
||||||
|
when " ", "\t"
|
||||||
|
i = handle_whitespace(result, i)
|
||||||
|
else
|
||||||
|
result += char
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_slash(_result, index)
|
||||||
|
if index + 1 < javascript.length
|
||||||
|
next_char = javascript[index + 1]
|
||||||
|
case next_char
|
||||||
|
when "/"
|
||||||
|
skip_line_comment(index)
|
||||||
|
when "*"
|
||||||
|
skip_block_comment(index)
|
||||||
|
else
|
||||||
|
javascript[index]
|
||||||
|
index + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
javascript[index]
|
||||||
|
index + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_string_literal(result, quote_char, index)
|
||||||
|
result += javascript[index]
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
index = process_string_content(result, quote_char, index)
|
||||||
|
append_closing_quote(result, index)
|
||||||
|
index + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_newline(result, index)
|
||||||
|
result << " " if preserve_space?(result)
|
||||||
|
skip_to_next_line(index)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_whitespace(result, index)
|
||||||
|
result << " " if preserve_space?(result)
|
||||||
|
skip_whitespace(index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Static site generator for homebrew tap documentation
|
||||||
class SiteBuilder
|
class SiteBuilder
|
||||||
include ERB::Util
|
include ERB::Util
|
||||||
|
include TimeFormatter
|
||||||
|
include AssetProcessor
|
||||||
|
|
||||||
|
# Context class for rendering ERB partials with access to builder methods and local variables
|
||||||
class PartialContext
|
class PartialContext
|
||||||
include ERB::Util
|
include ERB::Util
|
||||||
|
|
||||||
@@ -32,14 +328,14 @@ class SiteBuilder
|
|||||||
@builder.format_date(timestamp)
|
@builder.format_date(timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_binding
|
def binding_context
|
||||||
binding
|
binding
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
DOCS_DIR = File.expand_path('../docs', __dir__)
|
DOCS_DIR = File.expand_path("../docs", __dir__).freeze
|
||||||
DATA_DIR = File.join(DOCS_DIR, '_data')
|
DATA_DIR = File.join(DOCS_DIR, "_data").freeze
|
||||||
OUTPUT_DIR = DOCS_DIR
|
OUTPUT_DIR = DOCS_DIR
|
||||||
THEME_SOURCE_DIR = File.expand_path('../theme', __dir__)
|
THEME_SOURCE_DIR = File.expand_path("../theme", __dir__).freeze
|
||||||
TEMPLATES_DIR = THEME_SOURCE_DIR
|
TEMPLATES_DIR = THEME_SOURCE_DIR
|
||||||
|
|
||||||
def self.build
|
def self.build
|
||||||
@@ -55,76 +351,29 @@ class SiteBuilder
|
|||||||
generate_pages
|
generate_pages
|
||||||
|
|
||||||
puts "✅ Site built successfully in #{OUTPUT_DIR}"
|
puts "✅ Site built successfully in #{OUTPUT_DIR}"
|
||||||
puts "🌐 Open #{File.join(OUTPUT_DIR, 'index.html')} in your browser"
|
puts "🌐 Open #{File.join(OUTPUT_DIR, "index.html")} in your browser"
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_partial(name, locals = {})
|
def render_partial(name, locals = {})
|
||||||
partial_path = File.join(TEMPLATES_DIR, "_#{name}.html.erb")
|
partial_path = File.join(TEMPLATES_DIR, "_#{name}.html.erb")
|
||||||
unless File.exist?(partial_path)
|
raise ArgumentError, "Partial not found: #{partial_path}" unless File.exist?(partial_path)
|
||||||
raise ArgumentError, "Partial not found: #{partial_path}"
|
|
||||||
end
|
|
||||||
|
|
||||||
context = PartialContext.new(self, locals)
|
context = PartialContext.new(self, locals)
|
||||||
ERB.new(File.read(partial_path)).result(context.get_binding)
|
ERB.new(File.read(partial_path)).result(context.binding_context)
|
||||||
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def setup_directories
|
def setup_directories
|
||||||
FileUtils.mkdir_p(File.join(OUTPUT_DIR, 'formula'))
|
FileUtils.mkdir_p(File.join(OUTPUT_DIR, "formula"))
|
||||||
unless templates_exist?
|
return if templates_exist?
|
||||||
puts "⚠️ Templates not found in #{TEMPLATES_DIR}. Please ensure theme/*.html.erb files exist."
|
|
||||||
exit 1
|
puts "⚠️ Templates not found in #{TEMPLATES_DIR}. Please ensure theme/*.html.erb files exist."
|
||||||
end
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_data
|
def load_data
|
||||||
formulae_file = File.join(DATA_DIR, 'formulae.json')
|
formulae_file = File.join(DATA_DIR, "formulae.json")
|
||||||
@data = File.exist?(formulae_file) ? JSON.parse(File.read(formulae_file)) : default_data
|
@data = File.exist?(formulae_file) ? JSON.parse(File.read(formulae_file)) : default_data
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -139,170 +388,31 @@ class SiteBuilder
|
|||||||
generate_formulae_pages
|
generate_formulae_pages
|
||||||
end
|
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
|
def generate_index_page
|
||||||
template = load_template('index.html.erb')
|
template = load_template("index.html.erb")
|
||||||
content = template.result(binding)
|
content = template.result(binding)
|
||||||
File.write(File.join(OUTPUT_DIR, 'index.html'), content)
|
File.write(File.join(OUTPUT_DIR, "index.html"), content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_formulae_pages
|
def generate_formulae_pages
|
||||||
@data['formulae'].each do |formula|
|
@data["formulae"].each do |formula|
|
||||||
generate_formula_page(formula)
|
generate_formula_page(formula)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate formulae index
|
# Generate formulae index
|
||||||
template = load_template('formulae.html.erb')
|
template = load_template("formulae.html.erb")
|
||||||
content = template.result(binding)
|
content = template.result(binding)
|
||||||
File.write(File.join(OUTPUT_DIR, 'formulae.html'), content)
|
File.write(File.join(OUTPUT_DIR, "formulae.html"), content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_formula_page(formula)
|
def generate_formula_page(formula)
|
||||||
@formula = formula
|
@formula = formula
|
||||||
template = load_template('formula.html.erb')
|
template = load_template("formula.html.erb")
|
||||||
content = template.result(binding)
|
content = template.result(binding)
|
||||||
|
|
||||||
formula_dir = File.join(OUTPUT_DIR, 'formula')
|
formula_dir = File.join(OUTPUT_DIR, "formula")
|
||||||
FileUtils.mkdir_p(formula_dir)
|
FileUtils.mkdir_p(formula_dir)
|
||||||
File.write(File.join(formula_dir, "#{formula['name']}.html"), content)
|
File.write(File.join(formula_dir, "#{formula["name"]}.html"), content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template(name)
|
def load_template(name)
|
||||||
@@ -319,15 +429,13 @@ class SiteBuilder
|
|||||||
|
|
||||||
def default_data
|
def default_data
|
||||||
{
|
{
|
||||||
'tap_name' => 'ivuorinen/homebrew-tap',
|
"tap_name" => "ivuorinen/homebrew-tap",
|
||||||
'generated_at' => Time.now.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
"generated_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||||
'formulae_count' => 0,
|
"formulae_count" => 0,
|
||||||
'formulae' => []
|
"formulae" => [],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Allow running this script directly
|
# Allow running this script directly
|
||||||
if __FILE__ == $PROGRAM_NAME
|
SiteBuilder.build if __FILE__ == $PROGRAM_NAME
|
||||||
SiteBuilder.build
|
|
||||||
end
|
|
||||||
|
|||||||
49
scripts/make.rb
Normal file → Executable file
49
scripts/make.rb
Normal file → Executable file
@@ -1,16 +1,17 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
# typed: strict
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'fileutils'
|
require "fileutils"
|
||||||
|
|
||||||
# Simple make-style command runner for homebrew tap
|
# Simple make-style command runner for homebrew tap
|
||||||
class Make
|
class Make
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
'build' => 'Build the static site',
|
"build" => "Build the static site",
|
||||||
'serve' => 'Start development server',
|
"serve" => "Start development server",
|
||||||
'parse' => 'Parse formulae and generate JSON data',
|
"parse" => "Parse formulae and generate JSON data",
|
||||||
'clean' => 'Clean generated files',
|
"clean" => "Clean generated files",
|
||||||
'help' => 'Show this help message'
|
"help" => "Show this help message",
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def self.run(command = nil)
|
def self.run(command = nil)
|
||||||
@@ -19,15 +20,15 @@ class Make
|
|||||||
|
|
||||||
def execute(command)
|
def execute(command)
|
||||||
case command&.downcase
|
case command&.downcase
|
||||||
when 'build'
|
when "build"
|
||||||
build
|
build
|
||||||
when 'serve'
|
when "serve"
|
||||||
serve
|
serve
|
||||||
when 'parse'
|
when "parse"
|
||||||
parse
|
parse
|
||||||
when 'clean'
|
when "clean"
|
||||||
clean
|
clean
|
||||||
when 'help', nil
|
when "help", nil
|
||||||
help
|
help
|
||||||
else
|
else
|
||||||
puts "❌ Unknown command: #{command}"
|
puts "❌ Unknown command: #{command}"
|
||||||
@@ -41,10 +42,10 @@ class Make
|
|||||||
def build
|
def build
|
||||||
puts "🏗️ Building homebrew tap documentation..."
|
puts "🏗️ Building homebrew tap documentation..."
|
||||||
|
|
||||||
success = system('ruby', script_path('parse_formulas.rb'))
|
success = system("ruby", script_path("parse_formulas.rb"))
|
||||||
exit 1 unless success
|
exit 1 unless success
|
||||||
|
|
||||||
success = system('ruby', script_path('build_site.rb'))
|
success = system("ruby", script_path("build_site.rb"))
|
||||||
exit 1 unless success
|
exit 1 unless success
|
||||||
|
|
||||||
puts "✅ Build complete!"
|
puts "✅ Build complete!"
|
||||||
@@ -52,17 +53,17 @@ class Make
|
|||||||
|
|
||||||
def serve
|
def serve
|
||||||
port = ARGV[1]&.to_i || 4000
|
port = ARGV[1]&.to_i || 4000
|
||||||
host = ARGV[2] || 'localhost'
|
host = ARGV[2] || "localhost"
|
||||||
|
|
||||||
puts "🚀 Starting development server on http://#{host}:#{port}"
|
puts "🚀 Starting development server on http://#{host}:#{port}"
|
||||||
|
|
||||||
exec('ruby', script_path('serve.rb'), port.to_s, host)
|
exec("ruby", script_path("serve.rb"), port.to_s, host)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse
|
def parse
|
||||||
puts "📋 Parsing formulae..."
|
puts "📋 Parsing formulae..."
|
||||||
|
|
||||||
success = system('ruby', script_path('parse_formulas.rb'))
|
success = system("ruby", script_path("parse_formulas.rb"))
|
||||||
exit 1 unless success
|
exit 1 unless success
|
||||||
|
|
||||||
puts "✅ Formulae parsing complete!"
|
puts "✅ Formulae parsing complete!"
|
||||||
@@ -72,13 +73,13 @@ class Make
|
|||||||
puts "🧹 Cleaning generated files..."
|
puts "🧹 Cleaning generated files..."
|
||||||
|
|
||||||
files_to_clean = [
|
files_to_clean = [
|
||||||
docs_path('index.html'),
|
docs_path("index.html"),
|
||||||
docs_path('formulae.html'),
|
docs_path("formulae.html"),
|
||||||
docs_path('formula'),
|
docs_path("formula"),
|
||||||
docs_path('_templates'),
|
docs_path("_templates"),
|
||||||
docs_path('_data', 'formulae.json'),
|
docs_path("_data", "formulae.json"),
|
||||||
docs_path('style.css'),
|
docs_path("style.css"),
|
||||||
docs_path('main.js')
|
docs_path("main.js"),
|
||||||
]
|
]
|
||||||
|
|
||||||
files_to_clean.each do |path|
|
files_to_clean.each do |path|
|
||||||
@@ -113,7 +114,7 @@ class Make
|
|||||||
end
|
end
|
||||||
|
|
||||||
def docs_path(*parts)
|
def docs_path(*parts)
|
||||||
File.join(__dir__, '..', 'docs', *parts)
|
File.join(__dir__, "..", "docs", *parts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
# typed: strict
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'json'
|
require "json"
|
||||||
require 'fileutils'
|
require "fileutils"
|
||||||
require 'pathname'
|
require "pathname"
|
||||||
require 'date'
|
require "date"
|
||||||
|
|
||||||
|
# Simple polyfill for Homebrew extensions
|
||||||
|
class String
|
||||||
|
def blank?
|
||||||
|
# Polyfill implementation to avoid external dependencies
|
||||||
|
nil? || empty? # rubocop:disable Homebrew/Blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Parser class for extracting metadata from Homebrew formulae
|
# Parser class for extracting metadata from Homebrew formulae
|
||||||
class FormulaParser
|
class FormulaParser
|
||||||
FORMULA_DIR = File.expand_path('../Formula', __dir__)
|
FORMULA_DIR = File.expand_path("../Formula", __dir__).freeze
|
||||||
OUTPUT_DIR = File.expand_path('../docs/_data', __dir__)
|
OUTPUT_DIR = File.expand_path("../docs/_data", __dir__).freeze
|
||||||
OUTPUT_FILE = File.join(OUTPUT_DIR, 'formulae.json')
|
OUTPUT_FILE = File.join(OUTPUT_DIR, "formulae.json").freeze
|
||||||
|
|
||||||
# Regex patterns for safe extraction without code evaluation
|
# Regex patterns for safe extraction without code evaluation
|
||||||
PATTERNS = {
|
PATTERNS = {
|
||||||
class_name: /^class\s+(\w+)\s+<\s+Formula/,
|
class_name: /^class\s+(\w+)\s+<\s+Formula/,
|
||||||
desc: /^\s*desc\s+["']([^"']+)["']/,
|
desc: /^\s*desc\s+["']([^"']+)["']/,
|
||||||
homepage: /^\s*homepage\s+["']([^"']+)["']/,
|
homepage: /^\s*homepage\s+["']([^"']+)["']/,
|
||||||
url: /^\s*url\s+["']([^"']+)["']/,
|
url: /^\s*url\s+["']([^"']+)["']/,
|
||||||
version: /^\s*version\s+["']([^"']+)["']/,
|
version: /^\s*version\s+["']([^"']+)["']/,
|
||||||
sha256: /^\s*sha256\s+["']([a-f0-9]{64})["']/i,
|
sha256: /^\s*sha256\s+["']([a-f0-9]{64})["']/i,
|
||||||
license: /^\s*license\s+["']([^"']+)["']/,
|
license: /^\s*license\s+["']([^"']+)["']/,
|
||||||
depends_on: /^\s*depends_on\s+["']([^"']+)["']/
|
depends_on: /^\s*depends_on\s+["']([^"']+)["']/,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def self.run
|
def self.run
|
||||||
@@ -42,41 +51,49 @@ class FormulaParser
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parse_all_formulae
|
def parse_all_formulae
|
||||||
formula_files.map { |file| parse_formula(file) }.compact.sort_by { |f| f[:name] }
|
formula_files.filter_map { |file| parse_formula(file) }.sort_by { |f| f[:name] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def formula_files
|
def formula_files
|
||||||
Dir.glob(File.join(FORMULA_DIR, '**', '*.rb'))
|
Dir.glob(File.join(FORMULA_DIR, "**", "*.rb"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_formula(file_path)
|
def parse_formula(file_path)
|
||||||
content = File.read(file_path)
|
content = File.read(file_path)
|
||||||
class_name = extract_value(content, :class_name)
|
class_name = extract_value(content, :class_name)
|
||||||
|
|
||||||
return nil unless class_name
|
return unless class_name
|
||||||
|
|
||||||
formula_name = convert_class_name_to_formula_name(class_name)
|
formula_name = convert_class_name_to_formula_name(class_name)
|
||||||
|
|
||||||
return nil if formula_name.nil? || formula_name.empty?
|
return if formula_name.blank?
|
||||||
|
|
||||||
{
|
build_formula_metadata(content, file_path, formula_name, class_name)
|
||||||
name: formula_name,
|
rescue => e
|
||||||
class_name: class_name,
|
|
||||||
description: extract_value(content, :desc),
|
|
||||||
homepage: extract_value(content, :homepage),
|
|
||||||
url: extract_value(content, :url),
|
|
||||||
version: extract_version(content),
|
|
||||||
sha256: extract_value(content, :sha256),
|
|
||||||
license: extract_value(content, :license),
|
|
||||||
dependencies: extract_dependencies(content),
|
|
||||||
file_path: Pathname.new(file_path).relative_path_from(Pathname.new(FORMULA_DIR)).to_s,
|
|
||||||
last_modified: format_time_iso8601(File.mtime(file_path))
|
|
||||||
}
|
|
||||||
rescue StandardError => e
|
|
||||||
warn "⚠️ Error parsing #{file_path}: #{e.message}"
|
warn "⚠️ Error parsing #{file_path}: #{e.message}"
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_formula_metadata(content, file_path, formula_name, class_name)
|
||||||
|
{
|
||||||
|
name: formula_name,
|
||||||
|
class_name: class_name,
|
||||||
|
description: extract_value(content, :desc),
|
||||||
|
homepage: extract_value(content, :homepage),
|
||||||
|
url: extract_value(content, :url),
|
||||||
|
version: extract_version(content),
|
||||||
|
sha256: extract_value(content, :sha256),
|
||||||
|
license: extract_value(content, :license),
|
||||||
|
dependencies: extract_dependencies(content),
|
||||||
|
file_path: calculate_relative_path(file_path),
|
||||||
|
last_modified: format_time_iso8601(File.mtime(file_path)),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_relative_path(file_path)
|
||||||
|
Pathname.new(file_path).relative_path_from(Pathname.new(FORMULA_DIR)).to_s
|
||||||
|
end
|
||||||
|
|
||||||
def extract_value(content, pattern_key)
|
def extract_value(content, pattern_key)
|
||||||
match = content.match(PATTERNS[pattern_key])
|
match = content.match(PATTERNS[pattern_key])
|
||||||
match&.[](1)
|
match&.[](1)
|
||||||
@@ -88,7 +105,7 @@ class FormulaParser
|
|||||||
return explicit if explicit
|
return explicit if explicit
|
||||||
|
|
||||||
url = extract_value(content, :url)
|
url = extract_value(content, :url)
|
||||||
return nil unless url
|
return unless url
|
||||||
|
|
||||||
# Common version patterns in URLs
|
# Common version patterns in URLs
|
||||||
url.match(/v?(\d+(?:\.\d+)+)/)&.[](1)
|
url.match(/v?(\d+(?:\.\d+)+)/)&.[](1)
|
||||||
@@ -99,25 +116,25 @@ class FormulaParser
|
|||||||
end
|
end
|
||||||
|
|
||||||
def convert_class_name_to_formula_name(class_name)
|
def convert_class_name_to_formula_name(class_name)
|
||||||
return nil unless class_name
|
return unless class_name
|
||||||
|
|
||||||
# Convert CamelCase to kebab-case
|
# Convert CamelCase to kebab-case
|
||||||
class_name
|
class_name
|
||||||
.gsub(/([a-z\d])([A-Z])/, '\1-\2')
|
.gsub(/([a-z\d])([A-Z])/, "\1-\2")
|
||||||
.downcase
|
.downcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_time_iso8601(time)
|
def format_time_iso8601(time)
|
||||||
# Format time manually for compatibility
|
# Format time manually for compatibility
|
||||||
time.strftime('%Y-%m-%dT%H:%M:%S%z').gsub(/(\d{2})(\d{2})$/, '\1:\2')
|
time.strftime("%Y-%m-%dT%H:%M:%S%z").gsub(/(\d{2})(\d{2})$/, "\1:\2")
|
||||||
end
|
end
|
||||||
|
|
||||||
def write_json_output(formulae)
|
def write_json_output(formulae)
|
||||||
output = {
|
output = {
|
||||||
tap_name: 'ivuorinen/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,
|
||||||
}
|
}
|
||||||
|
|
||||||
File.write(OUTPUT_FILE, JSON.pretty_generate(output))
|
File.write(OUTPUT_FILE, JSON.pretty_generate(output))
|
||||||
|
|||||||
247
scripts/serve.rb
Normal file → Executable file
247
scripts/serve.rb
Normal file → Executable file
@@ -1,22 +1,146 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
# typed: strict
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'webrick'
|
require "webrick"
|
||||||
require 'fileutils'
|
require "fileutils"
|
||||||
require_relative 'parse_formulas'
|
require_relative "parse_formulas"
|
||||||
require_relative 'build_site'
|
require_relative "build_site"
|
||||||
|
|
||||||
# Simple development server for the homebrew tap documentation
|
# Simple development server for the homebrew tap documentation
|
||||||
|
# Module for handling file watching and change detection
|
||||||
|
module FileWatcher
|
||||||
|
def start_file_watcher
|
||||||
|
Thread.new do
|
||||||
|
last_mtime = max_mtime
|
||||||
|
rebuild_pending = false
|
||||||
|
watched_files = watched_files_count
|
||||||
|
|
||||||
|
puts "👀 Watching #{watched_files} files for changes..."
|
||||||
|
|
||||||
|
loop do
|
||||||
|
sleep 1
|
||||||
|
current_mtime = max_mtime
|
||||||
|
|
||||||
|
next if should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending)
|
||||||
|
|
||||||
|
rebuild_pending = true
|
||||||
|
handle_file_change(last_mtime)
|
||||||
|
last_mtime = perform_rebuild_with_debounce
|
||||||
|
rebuild_pending = false
|
||||||
|
puts "✅ Rebuild complete"
|
||||||
|
rescue => e
|
||||||
|
puts "⚠️ File watcher error: #{e.message}"
|
||||||
|
puts "📍 Backtrace: #{e.backtrace.first(3).join(", ")}"
|
||||||
|
rebuild_pending = false
|
||||||
|
sleep 2
|
||||||
|
puts "🔄 File watcher continuing..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def watched_files_count
|
||||||
|
all_watched_files.count { |file| !File.directory?(file) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_changed_file(since_mtime)
|
||||||
|
all_watched_files.find do |file|
|
||||||
|
File.exist?(file) && !File.directory?(file) && File.mtime(file) > since_mtime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_watched_files
|
||||||
|
[
|
||||||
|
formula_files,
|
||||||
|
theme_files,
|
||||||
|
template_files,
|
||||||
|
style_and_script_files,
|
||||||
|
asset_files,
|
||||||
|
build_script_files,
|
||||||
|
config_files,
|
||||||
|
].flatten.compact.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_mtime
|
||||||
|
all_watched_files
|
||||||
|
.select { |file| File.exist?(file) && !File.directory?(file) }
|
||||||
|
.map { |file| File.mtime(file) }
|
||||||
|
.max || Time.at(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def should_skip_rebuild?(current_mtime, last_mtime, rebuild_pending)
|
||||||
|
current_mtime <= last_mtime || rebuild_pending
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_file_change(last_mtime)
|
||||||
|
changed_file = find_changed_file(last_mtime)
|
||||||
|
puts "📝 Changed: #{changed_file}" if changed_file
|
||||||
|
puts "🔄 Rebuilding in 1 second..."
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_rebuild_with_debounce
|
||||||
|
sleep 1 # Debounce: wait for additional changes
|
||||||
|
final_mtime = max_mtime
|
||||||
|
puts "🔨 Building site..."
|
||||||
|
build_site
|
||||||
|
final_mtime
|
||||||
|
end
|
||||||
|
|
||||||
|
def formula_files
|
||||||
|
Dir.glob(File.expand_path("../Formula/**/*.rb", __dir__))
|
||||||
|
end
|
||||||
|
|
||||||
|
def theme_files
|
||||||
|
Dir.glob(File.expand_path("../theme/**/*", __dir__))
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_files
|
||||||
|
[
|
||||||
|
Dir.glob(File.expand_path("../theme/*.erb", __dir__)),
|
||||||
|
Dir.glob(File.expand_path("../theme/_*.erb", __dir__)),
|
||||||
|
Dir.glob(File.expand_path("../theme/*.html.erb", __dir__)),
|
||||||
|
Dir.glob(File.expand_path("../theme/_*.html.erb", __dir__)),
|
||||||
|
].flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def style_and_script_files
|
||||||
|
[
|
||||||
|
Dir.glob(File.expand_path("../theme/*.css", __dir__)),
|
||||||
|
Dir.glob(File.expand_path("../theme/*.js", __dir__)),
|
||||||
|
].flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_files
|
||||||
|
Dir.glob(File.expand_path("../theme/assets/**/*", __dir__))
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_script_files
|
||||||
|
[
|
||||||
|
File.expand_path("../scripts/parse_formulas.rb", __dir__),
|
||||||
|
File.expand_path("../scripts/build_site.rb", __dir__),
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_files
|
||||||
|
[File.expand_path("../Makefile", __dir__)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Development server with file watching for homebrew tap documentation
|
||||||
class DevServer
|
class DevServer
|
||||||
def self.serve(port: 4000, host: 'localhost')
|
include FileWatcher
|
||||||
|
|
||||||
|
def self.serve(port: 4000, host: "localhost")
|
||||||
new(port: port, host: host).start
|
new(port: port, host: host).start
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(port: 4000, host: 'localhost')
|
def initialize(port: 4000, host: "localhost")
|
||||||
@port = port
|
@port = port
|
||||||
@host = host
|
@host = host
|
||||||
@site_dir = File.expand_path('../docs', __dir__)
|
@site_dir = File.expand_path("../docs", __dir__)
|
||||||
@docs_dir = File.expand_path('../docs', __dir__)
|
@docs_dir = File.expand_path("../docs", __dir__)
|
||||||
end
|
end
|
||||||
|
|
||||||
def start
|
def start
|
||||||
@@ -43,128 +167,45 @@ class DevServer
|
|||||||
|
|
||||||
def start_server
|
def start_server
|
||||||
server = WEBrick::HTTPServer.new(
|
server = WEBrick::HTTPServer.new(
|
||||||
Port: @port,
|
Port: @port,
|
||||||
Host: @host,
|
Host: @host,
|
||||||
DocumentRoot: @site_dir,
|
DocumentRoot: @site_dir,
|
||||||
Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
|
Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
|
||||||
AccessLog: [[
|
AccessLog: [[
|
||||||
$stderr,
|
$stderr,
|
||||||
WEBrick::AccessLog::COMBINED_LOG_FORMAT
|
WEBrick::AccessLog::COMBINED_LOG_FORMAT,
|
||||||
]]
|
]],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle Ctrl+C gracefully
|
# Handle Ctrl+C gracefully
|
||||||
trap('INT') do
|
trap("INT") do
|
||||||
puts "\n👋 Stopping server..."
|
puts "\n👋 Stopping server..."
|
||||||
server.shutdown
|
server.shutdown
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add custom mime types if needed
|
# Add custom mime types if needed
|
||||||
server.config[:MimeTypes]['json'] = 'application/json'
|
server.config[:MimeTypes]["json"] = "application/json"
|
||||||
|
|
||||||
# Add auto-rebuild on file changes (simple polling)
|
# Add auto-rebuild on file changes (simple polling)
|
||||||
start_file_watcher
|
start_file_watcher
|
||||||
|
|
||||||
server.start
|
server.start
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Command line interface
|
# Command line interface
|
||||||
if __FILE__ == $PROGRAM_NAME
|
if __FILE__ == $PROGRAM_NAME
|
||||||
# Check for --list-watched flag
|
# Check for --list-watched flag
|
||||||
if ARGV.include?('--list-watched')
|
if ARGV.include?("--list-watched")
|
||||||
server = DevServer.new
|
server = DevServer.new
|
||||||
files = server.send(:get_all_watched_files).select { |f| File.exist?(f) && !File.directory?(f) }
|
files = server.all_watched_files.select { |f| File.exist?(f) && !File.directory?(f) }
|
||||||
puts "📋 Watching #{files.count} files:"
|
puts "📋 Watching #{files.count} files:"
|
||||||
files.sort.each { |f| puts " - #{f.sub(File.expand_path('..', __dir__) + '/', '')}" }
|
files.sort.each { |f| puts " - #{f.sub("#{File.expand_path("..", __dir__)}/", "")}" }
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
|
|
||||||
port = ARGV[0]&.to_i || 4000
|
port = ARGV[0]&.to_i || 4000
|
||||||
host = ARGV[1] || 'localhost'
|
host = ARGV[1] || "localhost"
|
||||||
|
|
||||||
DevServer.serve(port: port, host: host)
|
DevServer.serve(port: port, host: host)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSystemTheme() {
|
function getSystemTheme() {
|
||||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentTheme() {
|
function getCurrentTheme() {
|
||||||
@@ -137,8 +139,11 @@
|
|||||||
const cards = document.querySelectorAll(".formula-card");
|
const cards = document.querySelectorAll(".formula-card");
|
||||||
|
|
||||||
cards.forEach((card) => {
|
cards.forEach((card) => {
|
||||||
const text = `${card.querySelector("h3")?.textContent || ""} ${card.querySelector("p")?.textContent || ""}`;
|
const text = `${card.querySelector("h3")?.textContent || ""} ${
|
||||||
card.style.display = !searchTerm || fuzzyMatch(searchTerm, text) ? "" : "none";
|
card.querySelector("p")?.textContent || ""
|
||||||
|
}`;
|
||||||
|
card.style.display =
|
||||||
|
!searchTerm || fuzzyMatch(searchTerm, text) ? "" : "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
searchInput.addEventListener("input", () => {
|
searchInput.addEventListener("input", () => {
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Monaspace Argon";
|
font-family: "Monaspace Argon";
|
||||||
src:
|
src:
|
||||||
url("assets/MonaspaceArgonVar.woff2") format("woff2"),
|
url("assets/MonaspaceArgonVar.woff2") format("woff2"),
|
||||||
url("assets/MonaspaceArgonVar.woff") format("woff");
|
url("assets/MonaspaceArgonVar.woff") format("woff");
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family:
|
font-family:
|
||||||
"Monaspace Argon",
|
"Monaspace Argon",
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
"Segoe UI",
|
"Segoe UI",
|
||||||
Helvetica,
|
Helvetica,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
"Monaspace Argon",
|
"Monaspace Argon",
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
"Segoe UI",
|
"Segoe UI",
|
||||||
Helvetica,
|
Helvetica,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
pre {
|
pre {
|
||||||
font-family: "Monaspace Argon", monospace;
|
font-family: "Monaspace Argon", monospace;
|
||||||
font-feature-settings:
|
font-feature-settings:
|
||||||
"calt", "ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09", "ss10",
|
"calt", "ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08",
|
||||||
"liga";
|
"ss09", "ss10", "liga";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user