Ruby 4.0.1, update gems, fix lint warnings

This commit is contained in:
Sami Samhuri 2026-02-07 17:56:14 -08:00
parent fb07e8abba
commit 8b1e5e4349
No known key found for this signature in database
37 changed files with 583 additions and 580 deletions

View file

@ -1 +1 @@
3.4.1 4.0.1

28
Gemfile
View file

@ -1,19 +1,19 @@
source 'https://rubygems.org' source "https://rubygems.org"
ruby '~> 3.4.0' ruby "~> 4.0.1"
gem 'phlex', '~> 2.3' gem "phlex", "~> 2.3"
gem 'kramdown', '~> 2.5' gem "kramdown", "~> 2.5"
gem 'kramdown-parser-gfm', '~> 1.1' gem "kramdown-parser-gfm", "~> 1.1"
gem 'rouge', '~> 4.6' gem "rouge", "~> 4.6"
gem 'dry-struct', '~> 1.8' gem "dry-struct", "~> 1.8"
gem 'builder', '~> 3.3' gem "builder", "~> 3.3"
gem 'bake', '~> 0.20' gem "bake", "~> 0.20"
gem 'nokogiri', '~> 1.18' gem "nokogiri", "~> 1.18"
group :development, :test do group :development, :test do
gem 'rspec', '~> 3.13' gem "rspec", "~> 3.13"
gem 'guard', '~> 2.18' gem "guard", "~> 2.18"
gem 'guard-rspec', '~> 4.7' gem "guard-rspec", "~> 4.7"
gem 'standard', '~> 1.43' gem "standard", "~> 1.43"
end end

View file

@ -5,20 +5,20 @@ GEM
bake (0.24.1) bake (0.24.1)
bigdecimal bigdecimal
samovar (~> 2.1) samovar (~> 2.1)
bigdecimal (3.3.1) bigdecimal (4.0.1)
builder (3.3.0) builder (3.3.0)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.6)
console (1.34.2) console (1.34.2)
fiber-annotation fiber-annotation
fiber-local (~> 1.1) fiber-local (~> 1.1)
json json
diff-lcs (1.6.2) diff-lcs (1.6.2)
dry-core (1.1.0) dry-core (1.2.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
logger logger
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-inflector (1.2.0) dry-inflector (1.3.1)
dry-logic (1.6.0) dry-logic (1.6.0)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@ -29,38 +29,37 @@ GEM
dry-types (~> 1.8, >= 1.8.2) dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11) ice_nine (~> 0.11)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
dry-types (1.8.3) dry-types (1.9.1)
bigdecimal (~> 3.0) bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
dry-core (~> 1.0) dry-core (~> 1.0)
dry-inflector (~> 1.0) dry-inflector (~> 1.0)
dry-logic (~> 1.4) dry-logic (~> 1.4)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
ffi (1.17.2) ffi (1.17.3)
ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl) ffi (1.17.3-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu) ffi (1.17.3-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl) ffi (1.17.3-arm-linux-musl)
ffi (1.17.2-arm64-darwin) ffi (1.17.3-arm64-darwin)
ffi (1.17.2-x86-linux-gnu) ffi (1.17.3-x86-linux-gnu)
ffi (1.17.2-x86-linux-musl) ffi (1.17.3-x86-linux-musl)
ffi (1.17.2-x86_64-darwin) ffi (1.17.3-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl) ffi (1.17.3-x86_64-linux-musl)
fiber-annotation (0.2.0) fiber-annotation (0.2.0)
fiber-local (1.1.0) fiber-local (1.1.0)
fiber-storage fiber-storage
fiber-storage (1.0.1) fiber-storage (1.0.1)
formatador (1.2.3) formatador (1.2.3)
reline reline
guard (2.19.1) guard (2.20.1)
formatador (>= 0.2.4) formatador (>= 0.2.4)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
logger (~> 1.6) logger (~> 1.6)
lumberjack (>= 1.0.12, < 2.0) lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1) nenv (~> 0.1)
notiffany (~> 0.0) notiffany (~> 0.0)
ostruct (~> 0.6)
pry (>= 0.13.0) pry (>= 0.13.0)
shellany (~> 0.0) shellany (~> 0.0)
thor (>= 0.18.1) thor (>= 0.18.1)
@ -70,15 +69,16 @@ GEM
guard-compat (~> 1.1) guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0) rspec (>= 2.99.0, < 4.0)
ice_nine (0.11.2) ice_nine (0.11.2)
io-console (0.8.1) io-console (0.8.2)
json (2.16.0) json (2.18.1)
kramdown (2.5.1) kramdown (2.5.2)
rexml (>= 3.3.9) rexml (>= 3.4.4)
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
lint_roller (1.1.0) lint_roller (1.1.0)
listen (3.9.0) listen (3.10.0)
logger
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0) logger (1.7.0)
@ -87,49 +87,53 @@ GEM
method_source (1.1.0) method_source (1.1.0)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
nenv (0.3.0) nenv (0.3.0)
nokogiri (1.18.10) nokogiri (1.19.0)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-gnu) nokogiri (1.19.0-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl) nokogiri (1.19.0-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu) nokogiri (1.19.0-arm-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl) nokogiri (1.19.0-arm-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin) nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin) nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu) nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl) nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
notiffany (0.1.3) notiffany (0.1.3)
nenv (~> 0.1) nenv (~> 0.1)
shellany (~> 0.0) shellany (~> 0.0)
ostruct (0.6.3)
parallel (1.27.0) parallel (1.27.0)
parser (3.3.10.0) parser (3.3.10.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
phlex (2.3.1) phlex (2.4.1)
refract (~> 1.0)
zeitwerk (~> 2.7) zeitwerk (~> 2.7)
prism (1.6.0) prism (1.9.0)
pry (0.15.2) pry (0.16.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
reline (>= 0.6.0)
racc (1.8.1) racc (1.8.1)
rainbow (3.1.1) rainbow (3.1.1)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.11.1) rb-inotify (0.11.1)
ffi (~> 1.0) ffi (~> 1.0)
refract (1.1.0)
prism
zeitwerk
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.3) reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
rexml (3.4.4) rexml (3.4.4)
rouge (4.6.1) rouge (4.7.0)
rspec (3.13.2) rspec (3.13.2)
rspec-core (~> 3.13.0) rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0) rspec-expectations (~> 3.13.0)
@ -142,8 +146,8 @@ GEM
rspec-mocks (3.13.7) rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-support (3.13.6) rspec-support (3.13.7)
rubocop (1.80.2) rubocop (1.82.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -151,38 +155,38 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.46.0, < 2.0) rubocop-ast (>= 1.48.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0) rubocop-ast (1.49.0)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.4) prism (~> 1.7)
rubocop-performance (1.25.0) rubocop-performance (1.26.1)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
samovar (2.4.1) samovar (2.4.1)
console (~> 1.0) console (~> 1.0)
mapping (~> 1.0) mapping (~> 1.0)
shellany (0.0.1) shellany (0.0.1)
standard (1.51.1) standard (1.53.0)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0) lint_roller (~> 1.0)
rubocop (~> 1.80.2) rubocop (~> 1.82.0)
standard-custom (~> 1.0.0) standard-custom (~> 1.0.0)
standard-performance (~> 1.8) standard-performance (~> 1.8)
standard-custom (1.0.2) standard-custom (1.0.2)
lint_roller (~> 1.0) lint_roller (~> 1.0)
rubocop (~> 1.50) rubocop (~> 1.50)
standard-performance (1.8.0) standard-performance (1.9.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop-performance (~> 1.25.0) rubocop-performance (~> 1.26.0)
thor (1.4.0) thor (1.5.0)
unicode-display_width (3.2.0) unicode-display_width (3.2.0)
unicode-emoji (~> 4.1) unicode-emoji (~> 4.1)
unicode-emoji (4.1.0) unicode-emoji (4.2.0)
zeitwerk (2.7.3) zeitwerk (2.7.4)
PLATFORMS PLATFORMS
aarch64-linux-gnu aarch64-linux-gnu
@ -212,7 +216,7 @@ DEPENDENCIES
standard (~> 1.43) standard (~> 1.43)
RUBY VERSION RUBY VERSION
ruby 3.4.1p0 ruby 4.0.1p0
BUNDLED WITH BUNDLED WITH
2.6.2 4.0.3

123
bake.rb
View file

@ -1,42 +1,42 @@
# Build tasks for samhuri.net static site generator # Build tasks for samhuri.net static site generator
require 'etc' require "etc"
require 'fileutils' require "fileutils"
DRAFTS_DIR = 'public/drafts'.freeze DRAFTS_DIR = "public/drafts".freeze
PUBLISH_HOST = 'mudge'.freeze PUBLISH_HOST = "mudge".freeze
PRODUCTION_PUBLISH_DIR = '/var/www/samhuri.net/public'.freeze PRODUCTION_PUBLISH_DIR = "/var/www/samhuri.net/public".freeze
BETA_PUBLISH_DIR = '/var/www/beta.samhuri.net/public'.freeze BETA_PUBLISH_DIR = "/var/www/beta.samhuri.net/public".freeze
WATCHABLE_DIRECTORIES = %w[public posts lib].freeze WATCHABLE_DIRECTORIES = %w[public posts lib].freeze
LINT_TARGETS = %w[bake.rb bin Gemfile lib spec].freeze LINT_TARGETS = %w[bake.rb Gemfile lib spec].freeze
BUILD_TARGETS = %w[debug mudge beta release].freeze BUILD_TARGETS = %w[debug mudge beta release].freeze
# Generate the site in debug mode (localhost:8000) # Generate the site in debug mode (localhost:8000)
def debug def debug
build('http://localhost:8000') build("http://localhost:8000")
end end
# Generate the site for the mudge development server # Generate the site for the mudge development server
def mudge def mudge
build('http://mudge:8000') build("http://mudge:8000")
end end
# Generate the site for beta/staging # Generate the site for beta/staging
def beta def beta
build('https://beta.samhuri.net') build("https://beta.samhuri.net")
end end
# Generate the site for production # Generate the site for production
def release def release
build('https://samhuri.net') build("https://samhuri.net")
end end
# Start local development server # Start local development server
def serve def serve
require 'webrick' require "webrick"
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www') server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: "www")
trap('INT') { server.shutdown } trap("INT") { server.shutdown }
puts 'Server running at http://localhost:8000' puts "Server running at http://localhost:8000"
server.start server.start
end end
@ -45,11 +45,11 @@ end
def new_draft(*title_parts) def new_draft(*title_parts)
title, filename = title, filename =
if title_parts.empty? if title_parts.empty?
['Untitled', next_available_draft] ["Untitled", next_available_draft]
else else
given_title = title_parts.join(' ') given_title = title_parts.join(" ")
slug = slugify(given_title) slug = slugify(given_title)
abort 'Error: title cannot be converted to a filename.' if slug.empty? abort "Error: title cannot be converted to a filename." if slug.empty?
filename = "#{slug}.md" filename = "#{slug}.md"
path = draft_path(filename) path = draft_path(filename)
@ -64,7 +64,7 @@ def new_draft(*title_parts)
File.write(path, content) File.write(path, content)
puts "Created new draft at #{path}" puts "Created new draft at #{path}"
puts '>>> Contents below <<<' puts ">>> Contents below <<<"
puts puts
puts content puts content
end end
@ -73,12 +73,12 @@ end
# @parameter input_path [String] Draft path or filename in public/drafts. # @parameter input_path [String] Draft path or filename in public/drafts.
def publish_draft(input_path = nil) def publish_draft(input_path = nil)
if input_path.nil? || input_path.strip.empty? if input_path.nil? || input_path.strip.empty?
puts 'Usage: bake publish_draft <draft-path-or-filename>' puts "Usage: bake publish_draft <draft-path-or-filename>"
puts puts
puts 'Available drafts:' puts "Available drafts:"
drafts = Dir.glob("#{DRAFTS_DIR}/*.md").map { |path| File.basename(path) } drafts = Dir.glob("#{DRAFTS_DIR}/*.md").map { |path| File.basename(path) }
if drafts.empty? if drafts.empty?
puts ' (no drafts found)' puts " (no drafts found)"
else else
drafts.each { |draft| puts " #{draft}" } drafts.each { |draft| puts " #{draft}" }
end end
@ -91,9 +91,9 @@ def publish_draft(input_path = nil)
now = Time.now now = Time.now
content = File.read(draft_path_value) content = File.read(draft_path_value)
content.sub!(/^Date:.*$/, "Date: #{ordinal_date(now)}") content.sub!(/^Date:.*$/, "Date: #{ordinal_date(now)}")
content.sub!(/^Timestamp:.*$/, "Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}") content.sub!(/^Timestamp:.*$/, "Timestamp: #{now.strftime("%Y-%m-%dT%H:%M:%S%:z")}")
target_dir = "posts/#{now.strftime('%Y/%m')}" target_dir = "posts/#{now.strftime("%Y/%m")}"
FileUtils.mkdir_p(target_dir) FileUtils.mkdir_p(target_dir)
target_path = "#{target_dir}/#{draft_file}" target_path = "#{target_dir}/#{draft_file}"
@ -105,13 +105,13 @@ end
# Watch content directories and rebuild on every change. # Watch content directories and rebuild on every change.
# @parameter target [String] One of debug, mudge, beta, or release. # @parameter target [String] One of debug, mudge, beta, or release.
def watch(target: 'debug') def watch(target: "debug")
unless command_available?('inotifywait') unless command_available?("inotifywait")
abort 'inotifywait is required (install inotify-tools).' abort "inotifywait is required (install inotify-tools)."
end end
loop do loop do
abort 'Error: watch failed.' unless system('inotifywait', '-e', 'modify,create,delete,move', *watch_paths) abort "Error: watch failed." unless system("inotifywait", "-e", "modify,create,delete,move", *watch_paths)
puts "changed at #{Time.now}" puts "changed at #{Time.now}"
sleep 2 sleep 2
run_build_target(target) run_build_target(target)
@ -121,29 +121,29 @@ end
# Publish to beta/staging server # Publish to beta/staging server
def publish_beta def publish_beta
beta beta
run_rsync(local_paths: ['www/'], publish_dir: BETA_PUBLISH_DIR, dry_run: false, delete: true) run_rsync(local_paths: ["www/"], publish_dir: BETA_PUBLISH_DIR, dry_run: false, delete: true)
end end
# Publish to production server # Publish to production server
def publish def publish
release release
run_rsync(local_paths: ['www/'], publish_dir: PRODUCTION_PUBLISH_DIR, dry_run: false, delete: true) run_rsync(local_paths: ["www/"], publish_dir: PRODUCTION_PUBLISH_DIR, dry_run: false, delete: true)
end end
# Clean generated files # Clean generated files
def clean def clean
FileUtils.rm_rf('www') FileUtils.rm_rf("www")
puts 'Cleaned www/ directory' puts "Cleaned www/ directory"
end end
# Run RSpec tests # Run RSpec tests
def test def test
exec 'bundle exec rspec' exec "bundle exec rspec"
end end
# Run Guard for continuous testing # Run Guard for continuous testing
def guard def guard
exec 'bundle exec guard' exec "bundle exec guard"
end end
# List all available drafts # List all available drafts
@ -160,7 +160,7 @@ end
# Auto-fix StandardRB issues # Auto-fix StandardRB issues
def lint_fix def lint_fix
exec(*standardrb_command('--fix')) exec(*standardrb_command("--fix"))
end end
private private
@ -168,26 +168,26 @@ private
# Build the site with specified URL # Build the site with specified URL
# @parameter url [String] The site URL to use # @parameter url [String] The site URL to use
def build(url) def build(url)
require_relative 'lib/pressa' require_relative "lib/pressa"
puts "Building site for #{url}..." puts "Building site for #{url}..."
site = Pressa.create_site(source_path: '.', url_override: url) site = Pressa.create_site(source_path: ".", url_override: url)
generator = Pressa::SiteGenerator.new(site:) generator = Pressa::SiteGenerator.new(site:)
generator.generate(source_path: '.', target_path: 'www') generator.generate(source_path: ".", target_path: "www")
puts 'Site built successfully in www/' puts "Site built successfully in www/"
end end
def run_build_target(target) def run_build_target(target)
target_name = target.to_s target_name = target.to_s
unless BUILD_TARGETS.include?(target_name) unless BUILD_TARGETS.include?(target_name)
abort "Error: invalid target '#{target_name}'. Use one of: #{BUILD_TARGETS.join(', ')}" abort "Error: invalid target '#{target_name}'. Use one of: #{BUILD_TARGETS.join(", ")}"
end end
public_send(target_name) public_send(target_name)
end end
def watch_paths def watch_paths
WATCHABLE_DIRECTORIES.flat_map { |path| ['-r', path] } WATCHABLE_DIRECTORIES.flat_map { |path| ["-r", path] }
end end
def standardrb_command(*extra_args) def standardrb_command(*extra_args)
@ -195,17 +195,17 @@ def standardrb_command(*extra_args)
end end
def run_rsync(local_paths:, publish_dir:, dry_run:, delete:) def run_rsync(local_paths:, publish_dir:, dry_run:, delete:)
command = ['rsync', '-aKv', '-e', 'ssh -4'] command = ["rsync", "-aKv", "-e", "ssh -4"]
command << '--dry-run' if dry_run command << "--dry-run" if dry_run
command << '--delete' if delete command << "--delete" if delete
command.concat(local_paths) command.concat(local_paths)
command << "#{PUBLISH_HOST}:#{publish_dir}" command << "#{PUBLISH_HOST}:#{publish_dir}"
abort 'Error: rsync failed.' unless system(*command) abort "Error: rsync failed." unless system(*command)
end end
def resolve_draft_input(input_path) def resolve_draft_input(input_path)
if input_path.include?('/') if input_path.include?("/")
if input_path.start_with?('posts/') if input_path.start_with?("posts/")
abort "Error: '#{input_path}' is already published in posts/ directory" abort "Error: '#{input_path}' is already published in posts/ directory"
end end
@ -221,16 +221,15 @@ end
def slugify(title) def slugify(title)
title.downcase title.downcase
.gsub(/[^a-z0-9\s-]/, '') .gsub(/[^a-z0-9\s-]/, "")
.gsub(/\s+/, '-') .gsub(/\s+/, "-").squeeze("-")
.gsub(/-+/, '-') .gsub(/^-|-$/, "")
.gsub(/^-|-$/, '')
end end
def next_available_draft(base_filename = 'untitled.md') def next_available_draft(base_filename = "untitled.md")
return base_filename unless File.exist?(draft_path(base_filename)) return base_filename unless File.exist?(draft_path(base_filename))
name_without_ext = File.basename(base_filename, '.md') name_without_ext = File.basename(base_filename, ".md")
counter = 1 counter = 1
loop do loop do
numbered_filename = "#{name_without_ext}-#{counter}.md" numbered_filename = "#{name_without_ext}-#{counter}.md"
@ -247,7 +246,7 @@ def render_draft_template(title)
Author: #{current_author} Author: #{current_author}
Title: #{title} Title: #{title}
Date: unpublished Date: unpublished
Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')} Timestamp: #{now.strftime("%Y-%m-%dT%H:%M:%S%:z")}
Tags: Tags:
--- ---
@ -258,27 +257,27 @@ def render_draft_template(title)
end end
def current_author def current_author
Etc.getlogin || ENV['USER'] || `whoami`.strip Etc.getlogin || ENV["USER"] || `whoami`.strip
rescue StandardError rescue
ENV['USER'] || `whoami`.strip ENV["USER"] || `whoami`.strip
end end
def ordinal_date(time) def ordinal_date(time)
day = time.day day = time.day
suffix = case day suffix = case day
when 1, 21, 31 when 1, 21, 31
'st' "st"
when 2, 22 when 2, 22
'nd' "nd"
when 3, 23 when 3, 23
'rd' "rd"
else else
'th' "th"
end end
time.strftime("#{day}#{suffix} %B, %Y") time.strftime("#{day}#{suffix} %B, %Y")
end end
def command_available?(command) def command_available?(command)
system('which', command, out: File::NULL, err: File::NULL) system("which", command, out: File::NULL, err: File::NULL)
end end

View file

@ -1,8 +1,8 @@
require_relative '../site' require_relative "../site"
require_relative '../posts/plugin' require_relative "../posts/plugin"
require_relative '../projects/plugin' require_relative "../projects/plugin"
require_relative '../utils/markdown_renderer' require_relative "../utils/markdown_renderer"
require_relative 'simple_toml' require_relative "simple_toml"
module Pressa module Pressa
module Config module Config
@ -17,31 +17,31 @@ module Pressa
end end
def build_site(url_override: nil) def build_site(url_override: nil)
site_config = load_toml('site.toml') site_config = load_toml("site.toml")
projects_config = load_toml('projects.toml') projects_config = load_toml("projects.toml")
validate_required!(site_config, REQUIRED_SITE_KEYS, context: 'site.toml') validate_required!(site_config, REQUIRED_SITE_KEYS, context: "site.toml")
site_url = url_override || site_config['url'] site_url = url_override || site_config["url"]
projects_plugin = hash_or_empty(site_config['projects_plugin'], 'site.toml projects_plugin') projects_plugin = hash_or_empty(site_config["projects_plugin"], "site.toml projects_plugin")
projects = build_projects(projects_config) projects = build_projects(projects_config)
Site.new( Site.new(
author: site_config['author'], author: site_config["author"],
email: site_config['email'], email: site_config["email"],
title: site_config['title'], title: site_config["title"],
description: site_config['description'], description: site_config["description"],
url: site_url, url: site_url,
image_url: normalize_image_url(site_config['image_url'], site_url), image_url: normalize_image_url(site_config["image_url"], site_url),
scripts: build_scripts(site_config['scripts'], context: 'site.toml scripts'), scripts: build_scripts(site_config["scripts"], context: "site.toml scripts"),
styles: build_styles(site_config['styles'], context: 'site.toml styles'), styles: build_styles(site_config["styles"], context: "site.toml styles"),
plugins: [ plugins: [
Posts::Plugin.new, Posts::Plugin.new,
Projects::Plugin.new( Projects::Plugin.new(
projects:, projects:,
scripts: build_scripts(projects_plugin['scripts'], context: 'site.toml projects_plugin.scripts'), scripts: build_scripts(projects_plugin["scripts"], context: "site.toml projects_plugin.scripts"),
styles: build_styles(projects_plugin['styles'], context: 'site.toml projects_plugin.styles') styles: build_styles(projects_plugin["styles"], context: "site.toml projects_plugin.styles")
) )
], ],
renderers: [ renderers: [
@ -60,7 +60,7 @@ module Pressa
end end
def build_projects(projects_config) def build_projects(projects_config)
projects = projects_config['projects'] projects = projects_config["projects"]
raise ValidationError, "Missing required top-level array 'projects' in projects.toml" unless projects raise ValidationError, "Missing required top-level array 'projects' in projects.toml" unless projects
raise ValidationError, "Expected 'projects' to be an array in projects.toml" unless projects.is_a?(Array) raise ValidationError, "Expected 'projects' to be an array in projects.toml" unless projects.is_a?(Array)
@ -72,10 +72,10 @@ module Pressa
validate_required!(project, REQUIRED_PROJECT_KEYS, context: "projects.toml project ##{index + 1}") validate_required!(project, REQUIRED_PROJECT_KEYS, context: "projects.toml project ##{index + 1}")
Projects::Project.new( Projects::Project.new(
name: project['name'], name: project["name"],
title: project['title'], title: project["title"],
description: project['description'], description: project["description"],
url: project['url'] url: project["url"]
) )
end end
end end
@ -87,7 +87,7 @@ module Pressa
return if missing.empty? return if missing.empty?
raise ValidationError, "Missing required #{context} keys: #{missing.join(', ')}" raise ValidationError, "Missing required #{context} keys: #{missing.join(", ")}"
end end
def hash_or_empty(value, context) def hash_or_empty(value, context)
@ -105,10 +105,10 @@ module Pressa
when String when String
Script.new(src: item, defer: true) Script.new(src: item, defer: true)
when Hash when Hash
src = item['src'] src = item["src"]
raise ValidationError, "Expected #{context}[#{index}].src to be a String" unless src.is_a?(String) && !src.empty? raise ValidationError, "Expected #{context}[#{index}].src to be a String" unless src.is_a?(String) && !src.empty?
defer = item.key?('defer') ? item['defer'] : true defer = item.key?("defer") ? item["defer"] : true
unless [true, false].include?(defer) unless [true, false].include?(defer)
raise ValidationError, "Expected #{context}[#{index}].defer to be a Boolean" raise ValidationError, "Expected #{context}[#{index}].defer to be a Boolean"
end end
@ -128,7 +128,7 @@ module Pressa
when String when String
Stylesheet.new(href: item) Stylesheet.new(href: item)
when Hash when Hash
href = item['href'] href = item["href"]
raise ValidationError, "Expected #{context}[#{index}].href to be a String" unless href.is_a?(String) && !href.empty? raise ValidationError, "Expected #{context}[#{index}].href to be a String" unless href.is_a?(String) && !href.empty?
Stylesheet.new(href:) Stylesheet.new(href:)
@ -147,9 +147,9 @@ module Pressa
def normalize_image_url(value, site_url) def normalize_image_url(value, site_url)
return nil if value.nil? return nil if value.nil?
return value if value.start_with?('http://', 'https://') return value if value.start_with?("http://", "https://")
normalized = value.start_with?('/') ? value : "/#{value}" normalized = value.start_with?("/") ? value : "/#{value}"
"#{site_url}#{normalized}" "#{site_url}#{normalized}"
end end
end end

View file

@ -1,4 +1,4 @@
require 'json' require "json"
module Pressa module Pressa
module Config module Config
@ -89,7 +89,7 @@ module Pressa
keys.each do |key| keys.each do |key|
cursor[key] ||= {} cursor[key] ||= {}
unless cursor[key].is_a?(Hash) unless cursor[key].is_a?(Hash)
raise ParseError, "Expected table path '#{keys.join('.')}' at line #{line_number}" raise ParseError, "Expected table path '#{keys.join(".")}' at line #{line_number}"
end end
cursor = cursor[key] cursor = cursor[key]
@ -99,7 +99,7 @@ module Pressa
end end
def parse_path(raw_path, line_number) def parse_path(raw_path, line_number)
keys = raw_path.split('.').map(&:strip) keys = raw_path.split(".").map(&:strip)
if keys.empty? || keys.any? { |part| part.empty? || part !~ /\A[A-Za-z0-9_]+\z/ } if keys.empty? || keys.any? { |part| part.empty? || part !~ /\A[A-Za-z0-9_]+\z/ }
raise ParseError, "Invalid table path '#{raw_path}' at line #{line_number}" raise ParseError, "Invalid table path '#{raw_path}' at line #{line_number}"
end end
@ -107,7 +107,7 @@ module Pressa
end end
def parse_assignment(source, line_number) def parse_assignment(source, line_number)
separator = index_of_unquoted(source, '=') separator = index_of_unquoted(source, "=")
raise ParseError, "Invalid assignment at line #{line_number}" unless separator raise ParseError, "Invalid assignment at line #{line_number}" unless separator
key = source[0...separator].strip key = source[0...separator].strip
@ -136,7 +136,7 @@ module Pressa
if in_string if in_string
if escaped if escaped
escaped = false escaped = false
elsif char == '\\' elsif char == "\\"
escaped = true escaped = true
elsif char == '"' elsif char == '"'
in_string = false in_string = false
@ -148,9 +148,9 @@ module Pressa
case char case char
when '"' when '"'
in_string = true in_string = true
when '[', '{' when "[", "{"
depth += 1 depth += 1
when ']', '}' when "]", "}"
depth -= 1 depth -= 1
end end
end end
@ -159,7 +159,7 @@ module Pressa
end end
def strip_comments(line) def strip_comments(line)
output = +'' output = +""
in_string = false in_string = false
escaped = false escaped = false
@ -169,7 +169,7 @@ module Pressa
if escaped if escaped
escaped = false escaped = false
elsif char == '\\' elsif char == "\\"
escaped = true escaped = true
elsif char == '"' elsif char == '"'
in_string = false in_string = false
@ -182,7 +182,7 @@ module Pressa
when '"' when '"'
in_string = true in_string = true
output << char output << char
when '#' when "#"
break break
else else
output << char output << char
@ -200,7 +200,7 @@ module Pressa
if in_string if in_string
if escaped if escaped
escaped = false escaped = false
elsif char == '\\' elsif char == "\\"
escaped = true escaped = true
elsif char == '"' elsif char == '"'
in_string = false in_string = false

View file

@ -1,6 +1,6 @@
require 'json' require "json"
require_relative '../utils/file_writer' require_relative "../utils/file_writer"
require_relative '../views/feed_post_view' require_relative "../views/feed_post_view"
module Pressa module Pressa
module Posts module Posts
@ -18,7 +18,7 @@ module Pressa
feed = build_feed(recent) feed = build_feed(recent)
json = JSON.pretty_generate(feed) json = JSON.pretty_generate(feed)
file_path = File.join(target_path, 'feed.json') file_path = File.join(target_path, "feed.json")
Utils::FileWriter.write(path: file_path, content: json) Utils::FileWriter.write(path: file_path, content: json)
end end
@ -41,18 +41,18 @@ module Pressa
author:, author:,
version: FEED_VERSION, version: FEED_VERSION,
authors: [author], authors: [author],
feed_url: @site.url_for('/feed.json'), feed_url: @site.url_for("/feed.json"),
language: 'en-CA', language: "en-CA",
title: @site.title title: @site.title
} }
end end
def icon_url def icon_url
@site.url_for('/images/apple-touch-icon-300.png') @site.url_for("/images/apple-touch-icon-300.png")
end end
def favicon_url def favicon_url
@site.url_for('/images/apple-touch-icon-80.png') @site.url_for("/images/apple-touch-icon-80.png")
end end
def feed_item(post) def feed_item(post)
@ -65,7 +65,7 @@ module Pressa
item[:tags] = post.tags unless post.tags.empty? item[:tags] = post.tags unless post.tags.empty?
item[:content_html] = content_html item[:content_html] = content_html
item[:title] = post.link_post? ? "#{post.title}" : post.title item[:title] = post.link_post? ? "#{post.title}" : post.title
item[:author] = { name: post.author } item[:author] = {name: post.author}
item[:date_published] = post.date.iso8601 item[:date_published] = post.date.iso8601
item[:id] = permalink item[:id] = permalink

View file

@ -1,5 +1,5 @@
require 'yaml' require "yaml"
require 'date' require "date"
module Pressa module Pressa
module Posts module Posts
@ -28,36 +28,36 @@ module Pressa
def validate_required_fields! def validate_required_fields!
missing = REQUIRED_FIELDS.reject { |field| @raw.key?(field) } missing = REQUIRED_FIELDS.reject { |field| @raw.key?(field) }
raise "Missing required fields: #{missing.join(', ')}" unless missing.empty? raise "Missing required fields: #{missing.join(", ")}" unless missing.empty?
end end
def parse_fields def parse_fields
@title = @raw['Title'] @title = @raw["Title"]
@author = @raw['Author'] @author = @raw["Author"]
timestamp = @raw['Timestamp'] timestamp = @raw["Timestamp"]
@date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime @date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime
@formatted_date = @raw['Date'] @formatted_date = @raw["Date"]
@link = @raw['Link'] @link = @raw["Link"]
@tags = parse_tags(@raw['Tags']) @tags = parse_tags(@raw["Tags"])
@scripts = parse_scripts(@raw['Scripts']) @scripts = parse_scripts(@raw["Scripts"])
@styles = parse_styles(@raw['Styles']) @styles = parse_styles(@raw["Styles"])
end end
def parse_tags(value) def parse_tags(value)
return [] if value.nil? return [] if value.nil?
value.is_a?(Array) ? value : value.split(',').map(&:strip) value.is_a?(Array) ? value : value.split(",").map(&:strip)
end end
def parse_comma_separated(value) def parse_comma_separated(value)
return [] if value.nil? || value.empty? return [] if value.nil? || value.empty?
value.split(',').map(&:strip) value.split(",").map(&:strip)
end end
def parse_scripts(value) def parse_scripts(value)
return [] if value.nil? return [] if value.nil?
parse_comma_separated(value).map do |src| parse_comma_separated(value).map do |src|
Script.new(src: normalize_asset_path(src, 'js'), defer: true) Script.new(src: normalize_asset_path(src, "js"), defer: true)
end end
end end
@ -65,13 +65,13 @@ module Pressa
return [] if value.nil? return [] if value.nil?
parse_comma_separated(value).map do |href| parse_comma_separated(value).map do |href|
Stylesheet.new(href: normalize_asset_path(href, 'css')) Stylesheet.new(href: normalize_asset_path(href, "css"))
end end
end end
def normalize_asset_path(path, default_dir) def normalize_asset_path(path, default_dir)
return path if path.start_with?('http://', 'https://', '/') return path if path.start_with?("http://", "https://", "/")
return path if path.include?('/') return path if path.include?("/")
"#{default_dir}/#{path}" "#{default_dir}/#{path}"
end end

View file

@ -1,5 +1,5 @@
require 'dry-struct' require "dry-struct"
require_relative '../site' require_relative "../site"
module Pressa module Pressa
module Posts module Posts
@ -30,11 +30,11 @@ module Pressa
end end
def formatted_month def formatted_month
date.strftime('%B') date.strftime("%B")
end end
def padded_month def padded_month
format('%02d', month) format("%02d", month)
end end
end end
@ -45,9 +45,9 @@ module Pressa
def self.from_date(date) def self.from_date(date)
new( new(
name: date.strftime('%B'), name: date.strftime("%B"),
number: date.month, number: date.month,
padded: format('%02d', date.month) padded: format("%02d", date.month)
) )
end end
end end

View file

@ -1,8 +1,8 @@
require_relative '../plugin' require_relative "../plugin"
require_relative 'repo' require_relative "repo"
require_relative 'writer' require_relative "writer"
require_relative 'json_feed' require_relative "json_feed"
require_relative 'rss_feed' require_relative "rss_feed"
module Pressa module Pressa
module Posts module Posts
@ -10,7 +10,7 @@ module Pressa
attr_reader :posts_by_year attr_reader :posts_by_year
def setup(site:, source_path:) def setup(site:, source_path:)
posts_dir = File.join(source_path, 'posts') posts_dir = File.join(source_path, "posts")
return unless Dir.exist?(posts_dir) return unless Dir.exist?(posts_dir)
repo = PostRepo.new repo = PostRepo.new

View file

@ -1,13 +1,13 @@
require 'kramdown' require "kramdown"
require_relative 'models' require_relative "models"
require_relative 'metadata' require_relative "metadata"
module Pressa module Pressa
module Posts module Posts
class PostRepo class PostRepo
EXCERPT_LENGTH = 300 EXCERPT_LENGTH = 300
def initialize(output_path: 'posts') def initialize(output_path: "posts")
@output_path = output_path @output_path = output_path
@posts_by_year = {} @posts_by_year = {}
end end
@ -24,18 +24,18 @@ module Pressa
private private
def enumerate_markdown_files(dir, &block) def enumerate_markdown_files(dir, &block)
Dir.glob(File.join(dir, '**', '*.md')).each(&block) Dir.glob(File.join(dir, "**", "*.md")).each(&block)
end end
def read_post(file_path) def read_post(file_path)
content = File.read(file_path) content = File.read(file_path)
metadata = PostMetadata.parse(content) metadata = PostMetadata.parse(content)
body_markdown = content.sub(/\A---\s*\n.*?\n---\s*\n/m, '') body_markdown = content.sub(/\A---\s*\n.*?\n---\s*\n/m, "")
html_body = render_markdown(body_markdown) html_body = render_markdown(body_markdown)
slug = File.basename(file_path, '.md') slug = File.basename(file_path, ".md")
path = generate_path(slug, metadata.date) path = generate_path(slug, metadata.date)
excerpt = generate_excerpt(body_markdown) excerpt = generate_excerpt(body_markdown)
@ -58,9 +58,9 @@ module Pressa
def render_markdown(markdown) def render_markdown(markdown)
Kramdown::Document.new( Kramdown::Document.new(
markdown, markdown,
input: 'GFM', input: "GFM",
hard_wrap: false, hard_wrap: false,
syntax_highlighter: 'rouge', syntax_highlighter: "rouge",
syntax_highlighter_opts: { syntax_highlighter_opts: {
line_numbers: false, line_numbers: false,
wrap: true wrap: true
@ -70,27 +70,27 @@ module Pressa
def generate_path(slug, date) def generate_path(slug, date)
year = date.year year = date.year
month = format('%02d', date.month) month = format("%02d", date.month)
"/#{@output_path}/#{year}/#{month}/#{slug}" "/#{@output_path}/#{year}/#{month}/#{slug}"
end end
def generate_excerpt(markdown) def generate_excerpt(markdown)
text = markdown.dup text = markdown.dup
text.gsub!(/!\[[^\]]*\]\([^)]+\)/, '') text.gsub!(/!\[[^\]]*\]\([^)]+\)/, "")
text.gsub!(/!\[[^\]]*\]\[[^\]]+\]/, '') text.gsub!(/!\[[^\]]*\]\[[^\]]+\]/, "")
text.gsub!(/\[([^\]]+)\]\([^)]+\)/, '\1') text.gsub!(/\[([^\]]+)\]\([^)]+\)/, '\1')
text.gsub!(/\[([^\]]+)\]\[[^\]]+\]/, '\1') text.gsub!(/\[([^\]]+)\]\[[^\]]+\]/, '\1')
text.gsub!(/(?m)^\[[^\]]+\]:\s*\S.*$/, '') text.gsub!(/(?m)^\[[^\]]+\]:\s*\S.*$/, "")
text.gsub!(/<[^>]+>/, '') text.gsub!(/<[^>]+>/, "")
text.gsub!(/\s+/, ' ') text.gsub!(/\s+/, " ")
text.strip! text.strip!
return '...' if text.empty? return "..." if text.empty?
"#{text[0...EXCERPT_LENGTH]}..." "#{text[0...EXCERPT_LENGTH]}..."
end end

View file

@ -1,6 +1,6 @@
require 'builder' require "builder"
require_relative '../utils/file_writer' require_relative "../utils/file_writer"
require_relative '../views/feed_post_view' require_relative "../views/feed_post_view"
module Pressa module Pressa
module Posts module Posts
@ -16,15 +16,15 @@ module Pressa
xml = Builder::XmlMarkup.new(indent: 2) xml = Builder::XmlMarkup.new(indent: 2)
xml.instruct! :xml, version: "1.0", encoding: "UTF-8" xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
xml.rss version: "2.0", xml.rss :version => "2.0",
"xmlns:atom" => "http://www.w3.org/2005/Atom", "xmlns:atom" => "http://www.w3.org/2005/Atom",
"xmlns:content" => "http://purl.org/rss/1.0/modules/content/" do "xmlns:content" => "http://purl.org/rss/1.0/modules/content/" do
xml.channel do xml.channel do
xml.title @site.title xml.title @site.title
xml.link @site.url xml.link @site.url
xml.description @site.description xml.description @site.description
xml.pubDate recent.first.date.rfc822 if recent.any? xml.pubDate recent.first.date.rfc822 if recent.any?
xml.tag! "atom:link", href: @site.url_for('/feed.xml'), rel: "self", type: "application/rss+xml" xml.tag! "atom:link", href: @site.url_for("/feed.xml"), rel: "self", type: "application/rss+xml"
recent.each do |post| recent.each do |post|
xml.item do xml.item do
@ -35,13 +35,13 @@ module Pressa
xml.guid permalink, isPermaLink: "true" xml.guid permalink, isPermaLink: "true"
xml.pubDate post.date.rfc822 xml.pubDate post.date.rfc822
xml.author post.author xml.author post.author
xml.tag!('content:encoded') { xml.cdata!(render_feed_post(post)) } xml.tag!("content:encoded") { xml.cdata!(render_feed_post(post)) }
end end
end end
end end
end end
file_path = File.join(target_path, 'feed.xml') file_path = File.join(target_path, "feed.xml")
Utils::FileWriter.write(path: file_path, content: xml.target!) Utils::FileWriter.write(path: file_path, content: xml.target!)
end end

View file

@ -1,10 +1,10 @@
require_relative '../utils/file_writer' require_relative "../utils/file_writer"
require_relative '../views/layout' require_relative "../views/layout"
require_relative '../views/post_view' require_relative "../views/post_view"
require_relative '../views/recent_posts_view' require_relative "../views/recent_posts_view"
require_relative '../views/archive_view' require_relative "../views/archive_view"
require_relative '../views/year_posts_view' require_relative "../views/year_posts_view"
require_relative '../views/month_posts_view' require_relative "../views/month_posts_view"
module Pressa module Pressa
module Posts module Posts
@ -28,11 +28,11 @@ module Pressa
page_subtitle: nil, page_subtitle: nil,
canonical_url: @site.url, canonical_url: @site.url,
content: content_view, content: content_view,
page_description: 'Recent posts', page_description: "Recent posts",
page_type: 'article' page_type: "article"
) )
file_path = File.join(target_path, 'index.html') file_path = File.join(target_path, "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -40,13 +40,13 @@ module Pressa
content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site) content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site)
html = render_layout( html = render_layout(
page_subtitle: 'Archive', page_subtitle: "Archive",
canonical_url: @site.url_for('/posts/'), canonical_url: @site.url_for("/posts/"),
content: content_view, content: content_view,
page_description: 'Archive of all posts' page_description: "Archive of all posts"
) )
file_path = File.join(target_path, 'posts', 'index.html') file_path = File.join(target_path, "posts", "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -68,7 +68,7 @@ module Pressa
private private
def write_post(post:, target_path:) def write_post(post:, target_path:)
content_view = Views::PostView.new(post:, site: @site, article_class: 'container') content_view = Views::PostView.new(post:, site: @site, article_class: "container")
html = render_layout( html = render_layout(
page_subtitle: post.title, page_subtitle: post.title,
@ -77,10 +77,10 @@ module Pressa
page_scripts: post.scripts, page_scripts: post.scripts,
page_styles: post.styles, page_styles: post.styles,
page_description: post.excerpt, page_description: post.excerpt,
page_type: 'article' page_type: "article"
) )
file_path = File.join(target_path, post.path.sub(/^\//, ''), 'index.html') file_path = File.join(target_path, post.path.sub(/^\//, ""), "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -92,10 +92,10 @@ module Pressa
canonical_url: @site.url_for("/posts/#{year}/"), canonical_url: @site.url_for("/posts/#{year}/"),
content: content_view, content: content_view,
page_description: "Archive of all posts from #{year}", page_description: "Archive of all posts from #{year}",
page_type: 'article' page_type: "article"
) )
file_path = File.join(target_path, 'posts', year.to_s, 'index.html') file_path = File.join(target_path, "posts", year.to_s, "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -109,10 +109,10 @@ module Pressa
canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"), canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"),
content: content_view, content: content_view,
page_description: "Archive of all posts from #{title}", page_description: "Archive of all posts from #{title}",
page_type: 'article' page_type: "article"
) )
file_path = File.join(target_path, 'posts', year.to_s, month.padded, 'index.html') file_path = File.join(target_path, "posts", year.to_s, month.padded, "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -123,7 +123,7 @@ module Pressa
page_scripts: [], page_scripts: [],
page_styles: [], page_styles: [],
page_description: nil, page_description: nil,
page_type: 'website' page_type: "website"
) )
layout = Views::Layout.new( layout = Views::Layout.new(
site: @site, site: @site,

View file

@ -1,13 +1,13 @@
require_relative 'site' require_relative "site"
require_relative 'site_generator' require_relative "site_generator"
require_relative 'plugin' require_relative "plugin"
require_relative 'posts/plugin' require_relative "posts/plugin"
require_relative 'projects/plugin' require_relative "projects/plugin"
require_relative 'utils/markdown_renderer' require_relative "utils/markdown_renderer"
require_relative 'config/loader' require_relative "config/loader"
module Pressa module Pressa
def self.create_site(source_path: '.', url_override: nil) def self.create_site(source_path: ".", url_override: nil)
loader = Config::Loader.new(source_path:) loader = Config::Loader.new(source_path:)
loader.build_site(url_override:) loader.build_site(url_override:)
end end

View file

@ -1,5 +1,5 @@
require 'dry-struct' require "dry-struct"
require_relative '../site' require_relative "../site"
module Pressa module Pressa
module Projects module Projects
@ -11,7 +11,7 @@ module Pressa
def github_path def github_path
uri = URI.parse(url) uri = URI.parse(url)
uri.path.sub(/^\//, '') uri.path.sub(/^\//, "")
end end
def path def path

View file

@ -1,9 +1,9 @@
require_relative '../plugin' require_relative "../plugin"
require_relative '../utils/file_writer' require_relative "../utils/file_writer"
require_relative '../views/layout' require_relative "../views/layout"
require_relative '../views/projects_view' require_relative "../views/projects_view"
require_relative '../views/project_view' require_relative "../views/project_view"
require_relative 'models' require_relative "models"
module Pressa module Pressa
module Projects module Projects
@ -34,12 +34,12 @@ module Pressa
html = render_layout( html = render_layout(
site:, site:,
page_subtitle: 'Projects', page_subtitle: "Projects",
canonical_url: site.url_for('/projects/'), canonical_url: site.url_for("/projects/"),
content: content_view content: content_view
) )
file_path = File.join(target_path, 'projects', 'index.html') file_path = File.join(target_path, "projects", "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end
@ -56,7 +56,7 @@ module Pressa
page_description: project.description page_description: project.description
) )
file_path = File.join(target_path, 'projects', project.name, 'index.html') file_path = File.join(target_path, "projects", project.name, "index.html")
Utils::FileWriter.write(path: file_path, content: html) Utils::FileWriter.write(path: file_path, content: html)
end end

View file

@ -1,4 +1,4 @@
require 'dry-struct' require "dry-struct"
module Pressa module Pressa
module Types module Types

View file

@ -1,5 +1,5 @@
require 'fileutils' require "fileutils"
require_relative 'utils/file_writer' require_relative "utils/file_writer"
module Pressa module Pressa
class SiteGenerator class SiteGenerator
@ -30,7 +30,7 @@ module Pressa
target_abs = absolute_path(target_path) target_abs = absolute_path(target_path)
return unless contains_path?(container: target_abs, path: source_abs) return unless contains_path?(container: target_abs, path: source_abs)
raise ArgumentError, 'target_path must not be the same as or contain source_path' raise ArgumentError, "target_path must not be the same as or contain source_path"
end end
def absolute_path(path) def absolute_path(path)
@ -42,12 +42,12 @@ module Pressa
end end
def copy_static_files(source_path, target_path) def copy_static_files(source_path, target_path)
public_dir = File.join(source_path, 'public') public_dir = File.join(source_path, "public")
return unless Dir.exist?(public_dir) return unless Dir.exist?(public_dir)
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file| Dir.glob(File.join(public_dir, "**", "*"), File::FNM_DOTMATCH).each do |source_file|
next if File.directory?(source_file) next if File.directory?(source_file)
next if File.basename(source_file) == '.' || File.basename(source_file) == '..' next if File.basename(source_file) == "." || File.basename(source_file) == ".."
filename = File.basename(source_file) filename = File.basename(source_file)
ext = File.extname(source_file)[1..] ext = File.extname(source_file)[1..]
@ -56,7 +56,7 @@ module Pressa
next next
end end
relative_path = source_file.sub("#{public_dir}/", '') relative_path = source_file.sub("#{public_dir}/", "")
target_file = File.join(target_path, relative_path) target_file = File.join(target_path, relative_path)
FileUtils.mkdir_p(File.dirname(target_file)) FileUtils.mkdir_p(File.dirname(target_file))
@ -69,11 +69,11 @@ module Pressa
end end
def process_public_directory(source_path, target_path) def process_public_directory(source_path, target_path)
public_dir = File.join(source_path, 'public') public_dir = File.join(source_path, "public")
return unless Dir.exist?(public_dir) return unless Dir.exist?(public_dir)
site.renderers.each do |renderer| site.renderers.each do |renderer|
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file| Dir.glob(File.join(public_dir, "**", "*"), File::FNM_DOTMATCH).each do |source_file|
next if File.directory?(source_file) next if File.directory?(source_file)
filename = File.basename(source_file) filename = File.basename(source_file)
@ -82,10 +82,10 @@ module Pressa
if renderer.can_render_file?(filename:, extension: ext) if renderer.can_render_file?(filename:, extension: ext)
dir_name = File.dirname(source_file) dir_name = File.dirname(source_file)
relative_path = if dir_name == public_dir relative_path = if dir_name == public_dir
'' ""
else else
dir_name.sub("#{public_dir}/", '') dir_name.sub("#{public_dir}/", "")
end end
target_dir = File.join(target_path, relative_path) target_dir = File.join(target_path, relative_path)
renderer.render(site:, file_path: source_file, target_dir:) renderer.render(site:, file_path: source_file, target_dir:)

View file

@ -1,11 +1,11 @@
require 'fileutils' require "fileutils"
module Pressa module Pressa
module Utils module Utils
class FileWriter class FileWriter
def self.write(path:, content:, permissions: 0o644) def self.write(path:, content:, permissions: 0o644)
FileUtils.mkdir_p(File.dirname(path)) FileUtils.mkdir_p(File.dirname(path))
File.write(path, content, mode: 'w') File.write(path, content, mode: "w")
File.chmod(permissions, path) File.chmod(permissions, path)
end end

View file

@ -1,9 +1,9 @@
require 'kramdown' require "kramdown"
require 'yaml' require "yaml"
require_relative 'file_writer' require_relative "file_writer"
require_relative '../site' require_relative "../site"
require_relative '../views/layout' require_relative "../views/layout"
require_relative '../views/icons' require_relative "../views/icons"
module Pressa module Pressa
module Utils module Utils
@ -11,7 +11,7 @@ module Pressa
EXCERPT_LENGTH = 300 EXCERPT_LENGTH = 300
def can_render_file?(filename:, extension:) def can_render_file?(filename:, extension:)
extension == 'md' extension == "md"
end end
def render(site:, file_path:, target_dir:) def render(site:, file_path:, target_dir:)
@ -20,21 +20,21 @@ module Pressa
html_body = render_markdown(body_markdown) html_body = render_markdown(body_markdown)
page_title = presence(metadata['Title']) || File.basename(file_path, '.md').capitalize page_title = presence(metadata["Title"]) || File.basename(file_path, ".md").capitalize
page_type = presence(metadata['Page type']) || 'website' page_type = presence(metadata["Page type"]) || "website"
page_description = presence(metadata['Description']) || generate_excerpt(body_markdown) page_description = presence(metadata["Description"]) || generate_excerpt(body_markdown)
show_extension = ['true', 'yes', true].include?(metadata['Show extension']) show_extension = ["true", "yes", true].include?(metadata["Show extension"])
slug = File.basename(file_path, '.md') slug = File.basename(file_path, ".md")
relative_dir = File.dirname(file_path).sub(/^.*?\/public\/?/, '') relative_dir = File.dirname(file_path).sub(/^.*?\/public\/?/, "")
relative_dir = '' if relative_dir == '.' relative_dir = "" if relative_dir == "."
canonical_path = if show_extension canonical_path = if show_extension
"/#{relative_dir}/#{slug}.html".squeeze('/') "/#{relative_dir}/#{slug}.html".squeeze("/")
else else
"/#{relative_dir}/#{slug}/".squeeze('/') "/#{relative_dir}/#{slug}/".squeeze("/")
end end
html = render_layout( html = render_layout(
site:, site:,
@ -46,10 +46,10 @@ module Pressa
) )
output_filename = if show_extension output_filename = if show_extension
"#{slug}.html" "#{slug}.html"
else else
File.join(slug, 'index.html') File.join(slug, "index.html")
end end
output_path = File.join(target_dir, output_filename) output_path = File.join(target_dir, output_filename)
FileWriter.write(path: output_path, content: html) FileWriter.write(path: output_path, content: html)
@ -71,9 +71,9 @@ module Pressa
def render_markdown(markdown) def render_markdown(markdown)
Kramdown::Document.new( Kramdown::Document.new(
markdown, markdown,
input: 'GFM', input: "GFM",
hard_wrap: false, hard_wrap: false,
syntax_highlighter: 'rouge', syntax_highlighter: "rouge",
syntax_highlighter_opts: { syntax_highlighter_opts: {
line_numbers: false, line_numbers: false,
wrap: true wrap: true
@ -101,13 +101,13 @@ module Pressa
end end
def view_template def view_template
article(class: 'container') do article(class: "container") do
h1 { @page_title } h1 { @page_title }
raw(safe(@body)) raw(safe(@body))
end end
div(class: 'row clearfix') do div(class: "row clearfix") do
p(class: 'fin') do p(class: "fin") do
raw(safe(Views::Icons.code)) raw(safe(Views::Icons.code))
end end
end end
@ -118,18 +118,18 @@ module Pressa
text = markdown.dup text = markdown.dup
# Drop inline and reference-style images before links are simplified. # Drop inline and reference-style images before links are simplified.
text.gsub!(/!\[[^\]]*\]\([^)]+\)/, '') text.gsub!(/!\[[^\]]*\]\([^)]+\)/, "")
text.gsub!(/!\[[^\]]*\]\[[^\]]+\]/, '') text.gsub!(/!\[[^\]]*\]\[[^\]]+\]/, "")
# Replace inline and reference links with just their text. # Replace inline and reference links with just their text.
text.gsub!(/\[([^\]]+)\]\([^)]+\)/, '\1') text.gsub!(/\[([^\]]+)\]\([^)]+\)/, '\1')
text.gsub!(/\[([^\]]+)\]\[[^\]]+\]/, '\1') text.gsub!(/\[([^\]]+)\]\[[^\]]+\]/, '\1')
# Remove link reference definitions such as: [foo]: http://example.com # Remove link reference definitions such as: [foo]: http://example.com
text.gsub!(/(?m)^\[[^\]]+\]:\s*\S.*$/, '') text.gsub!(/(?m)^\[[^\]]+\]:\s*\S.*$/, "")
text.gsub!(/<[^>]+>/, '') text.gsub!(/<[^>]+>/, "")
text.gsub!(/\s+/, ' ') text.gsub!(/\s+/, " ")
text.strip! text.strip!
return nil if text.empty? return nil if text.empty?

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'year_posts_view' require_relative "year_posts_view"
module Pressa module Pressa
module Views module Views
@ -10,8 +10,8 @@ module Pressa
end end
def view_template def view_template
div(class: 'container') do div(class: "container") do
h1 { 'Archive' } h1 { "Archive" }
end end
@posts_by_year.sorted_years.each do |year| @posts_by_year.sorted_years.each do |year|

View file

@ -1,4 +1,4 @@
require 'phlex' require "phlex"
module Pressa module Pressa
module Views module Views
@ -10,10 +10,10 @@ module Pressa
def view_template def view_template
div do div do
p(class: 'time') { @post.formatted_date } p(class: "time") { @post.formatted_date }
raw(safe(@post.body)) raw(safe(@post.body))
p do p do
a(class: 'permalink', href: @site.url_for(@post.path)) { '∞' } a(class: "permalink", href: @site.url_for(@post.path)) { "" }
end end
end end
end end

View file

@ -4,19 +4,19 @@ module Pressa
module_function module_function
def mastodon def mastodon
svg(class_name: 'icon icon-mastodon', view_box: '0 0 448 512', path: IconPath::MASTODON) svg(class_name: "icon icon-mastodon", view_box: "0 0 448 512", path: IconPath::MASTODON)
end end
def github def github
svg(class_name: 'icon icon-github', view_box: '0 0 496 512', path: IconPath::GITHUB) svg(class_name: "icon icon-github", view_box: "0 0 496 512", path: IconPath::GITHUB)
end end
def rss def rss
svg(class_name: 'icon icon-rss', view_box: '0 0 448 512', path: IconPath::RSS) svg(class_name: "icon icon-rss", view_box: "0 0 448 512", path: IconPath::RSS)
end end
def code def code
svg(class_name: 'icon icon-code', view_box: '0 0 640 512', path: IconPath::CODE) svg(class_name: "icon icon-code", view_box: "0 0 640 512", path: IconPath::CODE)
end end
private_class_method def svg(class_name:, view_box:, path:) private_class_method def svg(class_name:, view_box:, path:)

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'icons' require_relative "icons"
module Pressa module Pressa
module Views module Views
@ -7,20 +7,19 @@ module Pressa
START_YEAR = 2006 START_YEAR = 2006
attr_reader :site, attr_reader :site,
:page_subtitle, :page_subtitle,
:page_description, :page_description,
:page_type, :page_type,
:canonical_url, :canonical_url,
:page_scripts, :page_scripts,
:page_styles, :page_styles,
:content :content
def initialize( def initialize(
site:, site:,
page_subtitle: nil, canonical_url:, page_subtitle: nil,
canonical_url:,
page_description: nil, page_description: nil,
page_type: 'website', page_type: "website",
page_scripts: [], page_scripts: [],
page_styles: [], page_styles: [],
content: nil content: nil
@ -42,54 +41,54 @@ module Pressa
def view_template def view_template
doctype doctype
html(lang: 'en') do html(lang: "en") do
comment { 'meow' } comment { "meow" }
head do head do
meta(charset: 'UTF-8') meta(charset: "UTF-8")
title { full_title } title { full_title }
meta(name: 'twitter:title', content: full_title) meta(name: "twitter:title", content: full_title)
meta(property: 'og:title', content: full_title) meta(property: "og:title", content: full_title)
meta(name: 'description', content: description) meta(name: "description", content: description)
meta(name: 'twitter:description', content: description) meta(name: "twitter:description", content: description)
meta(property: 'og:description', content: description) meta(property: "og:description", content: description)
meta(property: 'og:site_name', content: site.title) meta(property: "og:site_name", content: site.title)
link(rel: 'canonical', href: canonical_url) link(rel: "canonical", href: canonical_url)
meta(name: 'twitter:url', content: canonical_url) meta(name: "twitter:url", content: canonical_url)
meta(property: 'og:url', content: canonical_url) meta(property: "og:url", content: canonical_url)
meta(property: 'og:image', content: og_image_url) if og_image_url meta(property: "og:image", content: og_image_url) if og_image_url
meta(property: 'og:type', content: page_type) meta(property: "og:type", content: page_type)
meta(property: 'article:author', content: site.author) meta(property: "article:author", content: site.author)
meta(name: 'twitter:card', content: 'summary') meta(name: "twitter:card", content: "summary")
link( link(
rel: 'alternate', rel: "alternate",
href: site.url_for('/feed.xml'), href: site.url_for("/feed.xml"),
type: 'application/rss+xml', type: "application/rss+xml",
title: site.title title: site.title
) )
link( link(
rel: 'alternate', rel: "alternate",
href: site.url_for('/feed.json'), href: site.url_for("/feed.json"),
type: 'application/json', type: "application/json",
title: site.title title: site.title
) )
meta(name: 'fediverse:creator', content: '@sjs@techhub.social') meta(name: "fediverse:creator", content: "@sjs@techhub.social")
link(rel: 'author', type: 'text/plain', href: site.url_for('/humans.txt')) link(rel: "author", type: "text/plain", href: site.url_for("/humans.txt"))
link(rel: 'icon', type: 'image/png', href: site.url_for('/images/favicon-32x32.png')) link(rel: "icon", type: "image/png", href: site.url_for("/images/favicon-32x32.png"))
link(rel: 'shortcut icon', href: site.url_for('/images/favicon.icon')) link(rel: "shortcut icon", href: site.url_for("/images/favicon.icon"))
link(rel: 'apple-touch-icon', href: site.url_for('/images/apple-touch-icon.png')) link(rel: "apple-touch-icon", href: site.url_for("/images/apple-touch-icon.png"))
link(rel: 'mask-icon', color: '#aa0000', href: site.url_for('/images/safari-pinned-tab.svg')) link(rel: "mask-icon", color: "#aa0000", href: site.url_for("/images/safari-pinned-tab.svg"))
link(rel: 'manifest', href: site.url_for('/images/manifest.json')) link(rel: "manifest", href: site.url_for("/images/manifest.json"))
meta(name: 'msapplication-config', content: site.url_for('/images/browserconfig.xml')) meta(name: "msapplication-config", content: site.url_for("/images/browserconfig.xml"))
meta(name: 'theme-color', content: '#121212') meta(name: "theme-color", content: "#121212")
meta(name: 'viewport', content: 'width=device-width, initial-scale=1.0, viewport-fit=cover') meta(name: "viewport", content: "width=device-width, initial-scale=1.0, viewport-fit=cover")
link(rel: 'dns-prefetch', href: 'https://gist.github.com') link(rel: "dns-prefetch", href: "https://gist.github.com")
all_styles.each do |style| all_styles.each do |style|
link(rel: 'stylesheet', type: 'text/css', href: absolute_asset(style.href)) link(rel: "stylesheet", type: "text/css", href: absolute_asset(style.href))
end end
end end
@ -127,73 +126,73 @@ module Pressa
end end
def render_header def render_header
header(class: 'primary') do header(class: "primary") do
div(class: 'title') do div(class: "title") do
h1 do h1 do
a(href: site.url) { site.title } a(href: site.url) { site.title }
end end
br br
h4 do h4 do
plain 'By ' plain "By "
a(href: site.url_for('/about')) { site.author } a(href: site.url_for("/about")) { site.author }
end end
end end
nav(class: 'remote') do nav(class: "remote") do
ul do ul do
li(class: 'mastodon') do li(class: "mastodon") do
a(rel: 'me', 'aria-label': 'Mastodon', href: 'https://techhub.social/@sjs') do a(rel: "me", "aria-label": "Mastodon", href: "https://techhub.social/@sjs") do
raw(safe(Icons.mastodon)) raw(safe(Icons.mastodon))
end end
end end
li(class: 'github') do li(class: "github") do
a('aria-label': 'GitHub', href: 'https://github.com/samsonjs') do a("aria-label": "GitHub", href: "https://github.com/samsonjs") do
raw(safe(Icons.github)) raw(safe(Icons.github))
end end
end end
li(class: 'rss') do li(class: "rss") do
a('aria-label': 'RSS', href: site.url_for('/feed.xml')) do a("aria-label": "RSS", href: site.url_for("/feed.xml")) do
raw(safe(Icons.rss)) raw(safe(Icons.rss))
end end
end end
end end
end end
nav(class: 'local') do nav(class: "local") do
ul do ul do
li { a(href: site.url_for('/about')) { 'About' } } li { a(href: site.url_for("/about")) { "About" } }
li { a(href: site.url_for('/posts')) { 'Archive' } } li { a(href: site.url_for("/posts")) { "Archive" } }
li { a(href: site.url_for('/projects')) { 'Projects' } } li { a(href: site.url_for("/projects")) { "Projects" } }
end end
end end
div(class: 'clearfix') div(class: "clearfix")
end end
end end
def render_footer def render_footer
footer do footer do
plain "© #{START_YEAR} - #{Time.now.year} " plain "© #{START_YEAR} - #{Time.now.year} "
a(href: site.url_for('/about')) { site.author } a(href: site.url_for("/about")) { site.author }
end end
end end
def render_scripts def render_scripts
all_scripts.each do |scr| all_scripts.each do |scr|
attrs = { src: script_src(scr.src) } attrs = {src: script_src(scr.src)}
attrs[:defer] = true if scr.defer attrs[:defer] = true if scr.defer
script(**attrs) script(**attrs)
end end
end end
def script_src(src) def script_src(src)
return src if src.start_with?('http://', 'https://') return src if src.start_with?("http://", "https://")
absolute_asset(src) absolute_asset(src)
end end
def absolute_asset(path) def absolute_asset(path)
normalized = path.start_with?('/') ? path : "/#{path}" normalized = path.start_with?("/") ? path : "/#{path}"
site.url_for(normalized) site.url_for(normalized)
end end
end end

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'post_view' require_relative "post_view"
module Pressa module Pressa
module Views module Views
@ -11,12 +11,12 @@ module Pressa
end end
def view_template def view_template
div(class: 'container') do div(class: "container") do
h1 { "#{@month_posts.month.name} #{@year}" } h1 { "#{@month_posts.month.name} #{@year}" }
end end
@month_posts.sorted_posts.each do |post| @month_posts.sorted_posts.each do |post|
div(class: 'container') do div(class: "container") do
render PostView.new(post:, site: @site) render PostView.new(post:, site: @site)
end end
end end

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'icons' require_relative "icons"
module Pressa module Pressa
module Views module Views
@ -21,14 +21,14 @@ module Pressa
end end
end end
time { @post.formatted_date } time { @post.formatted_date }
a(href: @post.path, class: 'permalink') { '∞' } a(href: @post.path, class: "permalink") { "" }
end end
raw(safe(@post.body)) raw(safe(@post.body))
end end
div(class: 'row clearfix') do div(class: "row clearfix") do
p(class: 'fin') do p(class: "fin") do
raw(safe(Icons.code)) raw(safe(Icons.code))
end end
end end
@ -39,7 +39,7 @@ module Pressa
def article_attributes def article_attributes
return {} unless @article_class return {} unless @article_class
{ class: @article_class } {class: @article_class}
end end
end end
end end

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'icons' require_relative "icons"
module Pressa module Pressa
module Views module Views
@ -10,44 +10,43 @@ module Pressa
end end
def view_template def view_template
article(class: 'container project') do article(class: "container project") do
h1(id: 'project', data: { title: @project.title }) { @project.title } h1(id: "project", data: {title: @project.title}) { @project.title }
h4 { @project.description } h4 { @project.description }
div(class: 'project-stats') do div(class: "project-stats") do
p do p do
a(href: @project.url) { 'GitHub' } a(href: @project.url) { "GitHub" }
plain ' • ' plain ""
a(id: 'nstar', href: stargazers_url) a(id: "nstar", href: stargazers_url)
plain ' • ' plain ""
a(id: 'nfork', href: network_url) a(id: "nfork", href: network_url)
end end
p do p do
plain 'Last updated on ' plain "Last updated on "
span(id: 'updated') span(id: "updated")
end end
end end
div(class: 'project-info row clearfix') do div(class: "project-info row clearfix") do
div(class: 'column half') do div(class: "column half") do
h3 { 'Contributors' } h3 { "Contributors" }
div(id: 'contributors') div(id: "contributors")
end end
div(class: 'column half') do div(class: "column half") do
h3 { 'Languages' } h3 { "Languages" }
div(id: 'langs') div(id: "langs")
end end
end end
end end
div(class: 'row clearfix') do div(class: "row clearfix") do
p(class: 'fin') do p(class: "fin") do
raw(safe(Icons.code)) raw(safe(Icons.code))
end end
end end
end end
private private

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'icons' require_relative "icons"
module Pressa module Pressa
module Views module Views
@ -10,21 +10,21 @@ module Pressa
end end
def view_template def view_template
article(class: 'container') do article(class: "container") do
h1 { 'Projects' } h1 { "Projects" }
@projects.each do |project| @projects.each do |project|
div(class: 'project-listing') do div(class: "project-listing") do
h4 do h4 do
a(href: @site.url_for(project.path)) { project.title } a(href: @site.url_for(project.path)) { project.title }
end end
p(class: 'description') { project.description } p(class: "description") { project.description }
end end
end end
end end
div(class: 'row clearfix') do div(class: "row clearfix") do
p(class: 'fin') do p(class: "fin") do
raw(safe(Icons.code)) raw(safe(Icons.code))
end end
end end

View file

@ -1,5 +1,5 @@
require 'phlex' require "phlex"
require_relative 'post_view' require_relative "post_view"
module Pressa module Pressa
module Views module Views
@ -10,7 +10,7 @@ module Pressa
end end
def view_template def view_template
div(class: 'container') do div(class: "container") do
@posts.each do |post| @posts.each do |post|
render PostView.new(post:, site: @site) render PostView.new(post:, site: @site)
end end

View file

@ -1,4 +1,4 @@
require 'phlex' require "phlex"
module Pressa module Pressa
module Views module Views
@ -10,8 +10,8 @@ module Pressa
end end
def view_template def view_template
div(class: 'container') do div(class: "container") do
h2(class: 'year') do h2(class: "year") do
a(href: year_path) { @year.to_s } a(href: year_path) { @year.to_s }
end end
@ -30,13 +30,13 @@ module Pressa
def render_month(month_posts) def render_month(month_posts)
month = month_posts.month month = month_posts.month
h3(class: 'month') do h3(class: "month") do
a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do
month.name month.name
end end
end end
ul(class: 'archive') do ul(class: "archive") do
month_posts.sorted_posts.each do |post| month_posts.sorted_posts.each do |post|
li do li do
a(href: post_link(post)) { post.title } a(href: post_link(post)) { post.title }
@ -51,7 +51,7 @@ module Pressa
end end
def short_date(date) def short_date(date)
date.strftime('%-d %b') date.strftime("%-d %b")
end end
end end
end end

View file

@ -1,39 +1,39 @@
require 'spec_helper' require "spec_helper"
require 'fileutils' require "fileutils"
require 'tmpdir' require "tmpdir"
RSpec.describe Pressa::Config::Loader do RSpec.describe Pressa::Config::Loader do
describe '#build_site' do describe "#build_site" do
it 'builds a site from site.toml and projects.toml' do it "builds a site from site.toml and projects.toml" do
with_temp_config do |dir| with_temp_config do |dir|
loader = described_class.new(source_path: dir) loader = described_class.new(source_path: dir)
site = loader.build_site site = loader.build_site
expect(site.author).to eq('Sami Samhuri') expect(site.author).to eq("Sami Samhuri")
expect(site.url).to eq('https://samhuri.net') expect(site.url).to eq("https://samhuri.net")
expect(site.image_url).to eq('https://samhuri.net/images/me.jpg') expect(site.image_url).to eq("https://samhuri.net/images/me.jpg")
expect(site.styles.map(&:href)).to eq(['css/style.css']) expect(site.styles.map(&:href)).to eq(["css/style.css"])
projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) } projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) }
expect(projects_plugin).not_to be_nil expect(projects_plugin).not_to be_nil
expect(projects_plugin.scripts.map(&:src)).to eq(['js/projects.js']) expect(projects_plugin.scripts.map(&:src)).to eq(["js/projects.js"])
end end
end end
it 'applies url_override and rewrites relative image_url with override host' do it "applies url_override and rewrites relative image_url with override host" do
with_temp_config do |dir| with_temp_config do |dir|
loader = described_class.new(source_path: dir) loader = described_class.new(source_path: dir)
site = loader.build_site(url_override: 'https://beta.samhuri.net') site = loader.build_site(url_override: "https://beta.samhuri.net")
expect(site.url).to eq('https://beta.samhuri.net') expect(site.url).to eq("https://beta.samhuri.net")
expect(site.image_url).to eq('https://beta.samhuri.net/images/me.jpg') expect(site.image_url).to eq("https://beta.samhuri.net/images/me.jpg")
end end
end end
it 'raises a validation error for missing required site keys' do it "raises a validation error for missing required site keys" do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
File.write(File.join(dir, 'site.toml'), "title = \"x\"\n") File.write(File.join(dir, "site.toml"), "title = \"x\"\n")
File.write(File.join(dir, 'projects.toml'), '') File.write(File.join(dir, "projects.toml"), "")
loader = described_class.new(source_path: dir) loader = described_class.new(source_path: dir)
expect { loader.build_site }.to raise_error(Pressa::Config::ValidationError, /Missing required site\.toml keys/) expect { loader.build_site }.to raise_error(Pressa::Config::ValidationError, /Missing required site\.toml keys/)
@ -43,7 +43,7 @@ RSpec.describe Pressa::Config::Loader do
def with_temp_config def with_temp_config
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
File.write(File.join(dir, 'site.toml'), <<~TOML) File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri" author = "Sami Samhuri"
email = "sami@samhuri.net" email = "sami@samhuri.net"
title = "samhuri.net" title = "samhuri.net"
@ -58,7 +58,7 @@ RSpec.describe Pressa::Config::Loader do
styles = [] styles = []
TOML TOML
File.write(File.join(dir, 'projects.toml'), <<~TOML) File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]] [[projects]]
name = "demo" name = "demo"
title = "demo" title = "demo"

View file

@ -1,72 +1,72 @@
require 'spec_helper' require "spec_helper"
require 'json' require "json"
require 'tmpdir' require "tmpdir"
RSpec.describe Pressa::Posts::JSONFeedWriter do RSpec.describe Pressa::Posts::JSONFeedWriter do
let(:site) do let(:site) do
Pressa::Site.new( Pressa::Site.new(
author: 'Sami Samhuri', author: "Sami Samhuri",
email: 'sami@samhuri.net', email: "sami@samhuri.net",
title: 'samhuri.net', title: "samhuri.net",
description: 'blog', description: "blog",
url: 'https://samhuri.net', url: "https://samhuri.net",
image_url: 'https://samhuri.net/images/me.jpg' image_url: "https://samhuri.net/images/me.jpg"
) )
end end
let(:posts_by_year) { double('posts_by_year', recent_posts: [post]) } let(:posts_by_year) { double("posts_by_year", recent_posts: [post]) }
let(:writer) { described_class.new(site:, posts_by_year:) } let(:writer) { described_class.new(site:, posts_by_year:) }
context 'for link posts' do context "for link posts" do
let(:post) do let(:post) do
Pressa::Posts::Post.new( Pressa::Posts::Post.new(
slug: 'github-flow-like-a-pro', slug: "github-flow-like-a-pro",
title: 'GitHub Flow Like a Pro', title: "GitHub Flow Like a Pro",
author: 'Sami Samhuri', author: "Sami Samhuri",
date: DateTime.parse('2015-05-28T07:42:27-07:00'), date: DateTime.parse("2015-05-28T07:42:27-07:00"),
formatted_date: '28th May, 2015', formatted_date: "28th May, 2015",
link: 'http://haacked.com/archive/2014/07/28/github-flow-aliases/', link: "http://haacked.com/archive/2014/07/28/github-flow-aliases/",
body: '<p>hello</p>', body: "<p>hello</p>",
excerpt: 'hello...', excerpt: "hello...",
path: '/posts/2015/05/github-flow-like-a-pro' path: "/posts/2015/05/github-flow-like-a-pro"
) )
end end
it 'uses permalink as url and keeps external_url for destination links' do it "uses permalink as url and keeps external_url for destination links" do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
writer.write_feed(target_path: dir, limit: 30) writer.write_feed(target_path: dir, limit: 30)
feed = JSON.parse(File.read(File.join(dir, 'feed.json'))) feed = JSON.parse(File.read(File.join(dir, "feed.json")))
item = feed.fetch('items').first item = feed.fetch("items").first
expect(item.fetch('id')).to eq('https://samhuri.net/posts/2015/05/github-flow-like-a-pro') expect(item.fetch("id")).to eq("https://samhuri.net/posts/2015/05/github-flow-like-a-pro")
expect(item.fetch('url')).to eq('https://samhuri.net/posts/2015/05/github-flow-like-a-pro') expect(item.fetch("url")).to eq("https://samhuri.net/posts/2015/05/github-flow-like-a-pro")
expect(item.fetch('external_url')).to eq('http://haacked.com/archive/2014/07/28/github-flow-aliases/') expect(item.fetch("external_url")).to eq("http://haacked.com/archive/2014/07/28/github-flow-aliases/")
end end
end end
end end
context 'for regular posts' do context "for regular posts" do
let(:post) do let(:post) do
Pressa::Posts::Post.new( Pressa::Posts::Post.new(
slug: 'swift-optional-or', slug: "swift-optional-or",
title: 'Swift Optional OR', title: "Swift Optional OR",
author: 'Sami Samhuri', author: "Sami Samhuri",
date: DateTime.parse('2017-10-01T10:00:00-07:00'), date: DateTime.parse("2017-10-01T10:00:00-07:00"),
formatted_date: '1st October, 2017', formatted_date: "1st October, 2017",
body: '<p>hello</p>', body: "<p>hello</p>",
excerpt: 'hello...', excerpt: "hello...",
path: '/posts/2017/10/swift-optional-or' path: "/posts/2017/10/swift-optional-or"
) )
end end
it 'omits external_url' do it "omits external_url" do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
writer.write_feed(target_path: dir, limit: 30) writer.write_feed(target_path: dir, limit: 30)
feed = JSON.parse(File.read(File.join(dir, 'feed.json'))) feed = JSON.parse(File.read(File.join(dir, "feed.json")))
item = feed.fetch('items').first item = feed.fetch("items").first
expect(item.fetch('url')).to eq('https://samhuri.net/posts/2017/10/swift-optional-or') expect(item.fetch("url")).to eq("https://samhuri.net/posts/2017/10/swift-optional-or")
expect(item).not_to have_key('external_url') expect(item).not_to have_key("external_url")
end end
end end
end end

View file

@ -1,8 +1,8 @@
require 'spec_helper' require "spec_helper"
RSpec.describe Pressa::Posts::PostMetadata do RSpec.describe Pressa::Posts::PostMetadata do
describe '.parse' do describe ".parse" do
it 'parses valid YAML front-matter' do it "parses valid YAML front-matter" do
content = <<~MARKDOWN content = <<~MARKDOWN
--- ---
Title: Test Post Title: Test Post
@ -20,19 +20,19 @@ RSpec.describe Pressa::Posts::PostMetadata do
metadata = described_class.parse(content) metadata = described_class.parse(content)
expect(metadata.title).to eq('Test Post') expect(metadata.title).to eq("Test Post")
expect(metadata.author).to eq('Trent Reznor') expect(metadata.author).to eq("Trent Reznor")
expect(metadata.formatted_date).to eq('5th November, 2025') expect(metadata.formatted_date).to eq("5th November, 2025")
expect(metadata.date.year).to eq(2025) expect(metadata.date.year).to eq(2025)
expect(metadata.date.month).to eq(11) expect(metadata.date.month).to eq(11)
expect(metadata.date.day).to eq(5) expect(metadata.date.day).to eq(5)
expect(metadata.link).to eq('https://example.net/external') expect(metadata.link).to eq("https://example.net/external")
expect(metadata.tags).to eq(['Ruby', 'Testing']) expect(metadata.tags).to eq(["Ruby", "Testing"])
expect(metadata.scripts.map(&:src)).to eq(['js/highlight.js']) expect(metadata.scripts.map(&:src)).to eq(["js/highlight.js"])
expect(metadata.styles.map(&:href)).to eq(['css/code.css']) expect(metadata.styles.map(&:href)).to eq(["css/code.css"])
end end
it 'raises error when required fields are missing' do it "raises error when required fields are missing" do
content = <<~MARKDOWN content = <<~MARKDOWN
--- ---
Title: Incomplete Post Title: Incomplete Post
@ -46,7 +46,7 @@ RSpec.describe Pressa::Posts::PostMetadata do
}.to raise_error(/Missing required fields/) }.to raise_error(/Missing required fields/)
end end
it 'handles posts without optional fields' do it "handles posts without optional fields" do
content = <<~MARKDOWN content = <<~MARKDOWN
--- ---
Title: Simple Post Title: Simple Post

View file

@ -1,14 +1,14 @@
require 'spec_helper' require "spec_helper"
require 'fileutils' require "fileutils"
require 'tmpdir' require "tmpdir"
RSpec.describe Pressa::Posts::PostRepo do RSpec.describe Pressa::Posts::PostRepo do
let(:repo) { described_class.new } let(:repo) { described_class.new }
describe '#read_posts' do describe "#read_posts" do
it 'reads and organizes posts by year and month' do it "reads and organizes posts by year and month" do
Dir.mktmpdir do |tmpdir| Dir.mktmpdir do |tmpdir|
posts_dir = File.join(tmpdir, 'posts', '2025', '11') posts_dir = File.join(tmpdir, "posts", "2025", "11")
FileUtils.mkdir_p(posts_dir) FileUtils.mkdir_p(posts_dir)
post_content = <<~MARKDOWN post_content = <<~MARKDOWN
@ -22,25 +22,25 @@ RSpec.describe Pressa::Posts::PostRepo do
Had an epic day at Whistler. The powder was deep and the lines were short. Had an epic day at Whistler. The powder was deep and the lines were short.
MARKDOWN MARKDOWN
File.write(File.join(posts_dir, 'shredding.md'), post_content) File.write(File.join(posts_dir, "shredding.md"), post_content)
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts')) posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
expect(posts_by_year.all_posts.length).to eq(1) expect(posts_by_year.all_posts.length).to eq(1)
post = posts_by_year.all_posts.first post = posts_by_year.all_posts.first
expect(post.title).to eq('Shredding in November') expect(post.title).to eq("Shredding in November")
expect(post.author).to eq('Shaun White') expect(post.author).to eq("Shaun White")
expect(post.slug).to eq('shredding') expect(post.slug).to eq("shredding")
expect(post.year).to eq(2025) expect(post.year).to eq(2025)
expect(post.month).to eq(11) expect(post.month).to eq(11)
expect(post.path).to eq('/posts/2025/11/shredding') expect(post.path).to eq("/posts/2025/11/shredding")
end end
end end
it 'generates excerpts from post content' do it "generates excerpts from post content" do
Dir.mktmpdir do |tmpdir| Dir.mktmpdir do |tmpdir|
posts_dir = File.join(tmpdir, 'posts', '2025', '11') posts_dir = File.join(tmpdir, "posts", "2025", "11")
FileUtils.mkdir_p(posts_dir) FileUtils.mkdir_p(posts_dir)
post_content = <<~MARKDOWN post_content = <<~MARKDOWN
@ -58,15 +58,15 @@ RSpec.describe Pressa::Posts::PostRepo do
More content with a [link](https://example.net). More content with a [link](https://example.net).
MARKDOWN MARKDOWN
File.write(File.join(posts_dir, 'test.md'), post_content) File.write(File.join(posts_dir, "test.md"), post_content)
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts')) posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
post = posts_by_year.all_posts.first post = posts_by_year.all_posts.first
expect(post.excerpt).to include('test post') expect(post.excerpt).to include("test post")
expect(post.excerpt).not_to include('![') expect(post.excerpt).not_to include("![")
expect(post.excerpt).to include('link') expect(post.excerpt).to include("link")
expect(post.excerpt).not_to include('[link]') expect(post.excerpt).not_to include("[link]")
end end
end end
end end

View file

@ -1,25 +1,25 @@
require 'spec_helper' require "spec_helper"
require 'fileutils' require "fileutils"
require 'tmpdir' require "tmpdir"
RSpec.describe Pressa::SiteGenerator do RSpec.describe Pressa::SiteGenerator do
let(:site) do let(:site) do
Pressa::Site.new( Pressa::Site.new(
author: 'Sami Samhuri', author: "Sami Samhuri",
email: 'sami@samhuri.net', email: "sami@samhuri.net",
title: 'samhuri.net', title: "samhuri.net",
description: 'blog', description: "blog",
url: 'https://samhuri.net', url: "https://samhuri.net",
plugins: [], plugins: [],
renderers: [] renderers: []
) )
end end
it 'rejects a target path that matches the source path' do it "rejects a target path that matches the source path" do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
FileUtils.mkdir_p(File.join(dir, 'public')) FileUtils.mkdir_p(File.join(dir, "public"))
source_file = File.join(dir, 'public', 'keep.txt') source_file = File.join(dir, "public", "keep.txt")
File.write(source_file, 'safe') File.write(source_file, "safe")
generator = described_class.new(site:) generator = described_class.new(site:)
@ -27,7 +27,7 @@ RSpec.describe Pressa::SiteGenerator do
generator.generate(source_path: dir, target_path: dir) generator.generate(source_path: dir, target_path: dir)
}.to raise_error(ArgumentError, /must not be the same as or contain source_path/) }.to raise_error(ArgumentError, /must not be the same as or contain source_path/)
expect(File.read(source_file)).to eq('safe') expect(File.read(source_file)).to eq("safe")
end end
end end
end end

View file

@ -1,4 +1,4 @@
require_relative '../lib/pressa' require_relative "../lib/pressa"
RSpec.configure do |config| RSpec.configure do |config|
config.expect_with :rspec do |expectations| config.expect_with :rspec do |expectations|

View file

@ -1,45 +1,47 @@
require 'spec_helper' require "spec_helper"
RSpec.describe Pressa::Views::Layout do RSpec.describe Pressa::Views::Layout do
class TestContentView < Phlex::HTML let(:test_content_view) do
def view_template Class.new(Phlex::HTML) do
article do def view_template
h1 { 'Hello' } article do
h1 { "Hello" }
end
end end
end end.new
end end
let(:site) do let(:site) do
Pressa::Site.new( Pressa::Site.new(
author: 'Sami Samhuri', author: "Sami Samhuri",
email: 'sami@samhuri.net', email: "sami@samhuri.net",
title: 'samhuri.net', title: "samhuri.net",
description: 'blog', description: "blog",
url: 'https://samhuri.net' url: "https://samhuri.net"
) )
end end
it 'renders child components as HTML instead of escaped text' do it "renders child components as HTML instead of escaped text" do
html = described_class.new( html = described_class.new(
site:, site:,
canonical_url: 'https://samhuri.net/posts/', canonical_url: "https://samhuri.net/posts/",
content: TestContentView.new content: test_content_view
).call ).call
expect(html).to include('<article>') expect(html).to include("<article>")
expect(html).to include('<h1>Hello</h1>') expect(html).to include("<h1>Hello</h1>")
expect(html).not_to include('&lt;article&gt;') expect(html).not_to include("&lt;article&gt;")
end end
it 'keeps escaping enabled for untrusted string fields' do it "keeps escaping enabled for untrusted string fields" do
subtitle = '<img src=x onerror=alert(1)>' subtitle = "<img src=x onerror=alert(1)>"
html = described_class.new( html = described_class.new(
site:, site:,
canonical_url: 'https://samhuri.net/posts/', canonical_url: "https://samhuri.net/posts/",
page_subtitle: subtitle, page_subtitle: subtitle,
content: TestContentView.new content: test_content_view
).call ).call
expect(html).to include('<title>samhuri.net: &lt;img src=x onerror=alert(1)&gt;</title>') expect(html).to include("<title>samhuri.net: &lt;img src=x onerror=alert(1)&gt;</title>")
end end
end end