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 'kramdown', '~> 2.5'
gem 'kramdown-parser-gfm', '~> 1.1'
gem 'rouge', '~> 4.6'
gem 'dry-struct', '~> 1.8'
gem 'builder', '~> 3.3'
gem 'bake', '~> 0.20'
gem 'nokogiri', '~> 1.18'
gem "phlex", "~> 2.3"
gem "kramdown", "~> 2.5"
gem "kramdown-parser-gfm", "~> 1.1"
gem "rouge", "~> 4.6"
gem "dry-struct", "~> 1.8"
gem "builder", "~> 3.3"
gem "bake", "~> 0.20"
gem "nokogiri", "~> 1.18"
group :development, :test do
gem 'rspec', '~> 3.13'
gem 'guard', '~> 2.18'
gem 'guard-rspec', '~> 4.7'
gem 'standard', '~> 1.43'
gem "rspec", "~> 3.13"
gem "guard", "~> 2.18"
gem "guard-rspec", "~> 4.7"
gem "standard", "~> 1.43"
end

View file

@ -5,20 +5,20 @@ GEM
bake (0.24.1)
bigdecimal
samovar (~> 2.1)
bigdecimal (3.3.1)
bigdecimal (4.0.1)
builder (3.3.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
concurrent-ruby (1.3.6)
console (1.34.2)
fiber-annotation
fiber-local (~> 1.1)
json
diff-lcs (1.6.2)
dry-core (1.1.0)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-inflector (1.3.1)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
@ -29,38 +29,37 @@ GEM
dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
dry-types (1.9.1)
bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
ffi (1.17.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86-linux-gnu)
ffi (1.17.2-x86-linux-musl)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
ffi (1.17.3)
ffi (1.17.3-aarch64-linux-gnu)
ffi (1.17.3-aarch64-linux-musl)
ffi (1.17.3-arm-linux-gnu)
ffi (1.17.3-arm-linux-musl)
ffi (1.17.3-arm64-darwin)
ffi (1.17.3-x86-linux-gnu)
ffi (1.17.3-x86-linux-musl)
ffi (1.17.3-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
fiber-storage
fiber-storage (1.0.1)
formatador (1.2.3)
reline
guard (2.19.1)
guard (2.20.1)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
logger (~> 1.6)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
ostruct (~> 0.6)
pry (>= 0.13.0)
shellany (~> 0.0)
thor (>= 0.18.1)
@ -70,15 +69,16 @@ GEM
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
ice_nine (0.11.2)
io-console (0.8.1)
json (2.16.0)
kramdown (2.5.1)
rexml (>= 3.3.9)
io-console (0.8.2)
json (2.18.1)
kramdown (2.5.2)
rexml (>= 3.4.4)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
listen (3.9.0)
listen (3.10.0)
logger
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
@ -87,49 +87,53 @@ GEM
method_source (1.1.0)
mini_portile2 (2.8.9)
nenv (0.3.0)
nokogiri (1.18.10)
nokogiri (1.19.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-gnu)
nokogiri (1.19.0-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
nokogiri (1.19.0-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
nokogiri (1.19.0-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
nokogiri (1.19.0-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4)
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.0)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
phlex (2.3.1)
phlex (2.4.1)
refract (~> 1.0)
zeitwerk (~> 2.7)
prism (1.6.0)
pry (0.15.2)
prism (1.9.0)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
reline (>= 0.6.0)
racc (1.8.1)
rainbow (3.1.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
refract (1.1.0)
prism
zeitwerk
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
rexml (3.4.4)
rouge (4.6.1)
rouge (4.7.0)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@ -142,8 +146,8 @@ GEM
rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.6)
rubocop (1.80.2)
rspec-support (3.13.7)
rubocop (1.82.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -151,38 +155,38 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.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)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.25.0)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
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)
samovar (2.4.1)
console (~> 1.0)
mapping (~> 1.0)
shellany (0.0.1)
standard (1.51.1)
standard (1.53.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.80.2)
rubocop (~> 1.82.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.8.0)
standard-performance (1.9.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.25.0)
thor (1.4.0)
rubocop-performance (~> 1.26.0)
thor (1.5.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
zeitwerk (2.7.3)
unicode-emoji (4.2.0)
zeitwerk (2.7.4)
PLATFORMS
aarch64-linux-gnu
@ -212,7 +216,7 @@ DEPENDENCIES
standard (~> 1.43)
RUBY VERSION
ruby 3.4.1p0
ruby 4.0.1p0
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
require 'etc'
require 'fileutils'
require "etc"
require "fileutils"
DRAFTS_DIR = 'public/drafts'.freeze
PUBLISH_HOST = 'mudge'.freeze
PRODUCTION_PUBLISH_DIR = '/var/www/samhuri.net/public'.freeze
BETA_PUBLISH_DIR = '/var/www/beta.samhuri.net/public'.freeze
DRAFTS_DIR = "public/drafts".freeze
PUBLISH_HOST = "mudge".freeze
PRODUCTION_PUBLISH_DIR = "/var/www/samhuri.net/public".freeze
BETA_PUBLISH_DIR = "/var/www/beta.samhuri.net/public".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
# Generate the site in debug mode (localhost:8000)
def debug
build('http://localhost:8000')
build("http://localhost:8000")
end
# Generate the site for the mudge development server
def mudge
build('http://mudge:8000')
build("http://mudge:8000")
end
# Generate the site for beta/staging
def beta
build('https://beta.samhuri.net')
build("https://beta.samhuri.net")
end
# Generate the site for production
def release
build('https://samhuri.net')
build("https://samhuri.net")
end
# Start local development server
def serve
require 'webrick'
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
trap('INT') { server.shutdown }
puts 'Server running at http://localhost:8000'
require "webrick"
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: "www")
trap("INT") { server.shutdown }
puts "Server running at http://localhost:8000"
server.start
end
@ -45,11 +45,11 @@ end
def new_draft(*title_parts)
title, filename =
if title_parts.empty?
['Untitled', next_available_draft]
["Untitled", next_available_draft]
else
given_title = title_parts.join(' ')
given_title = title_parts.join(" ")
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"
path = draft_path(filename)
@ -64,7 +64,7 @@ def new_draft(*title_parts)
File.write(path, content)
puts "Created new draft at #{path}"
puts '>>> Contents below <<<'
puts ">>> Contents below <<<"
puts
puts content
end
@ -73,12 +73,12 @@ end
# @parameter input_path [String] Draft path or filename in public/drafts.
def publish_draft(input_path = nil)
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 'Available drafts:'
puts "Available drafts:"
drafts = Dir.glob("#{DRAFTS_DIR}/*.md").map { |path| File.basename(path) }
if drafts.empty?
puts ' (no drafts found)'
puts " (no drafts found)"
else
drafts.each { |draft| puts " #{draft}" }
end
@ -91,9 +91,9 @@ def publish_draft(input_path = nil)
now = Time.now
content = File.read(draft_path_value)
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)
target_path = "#{target_dir}/#{draft_file}"
@ -105,13 +105,13 @@ end
# Watch content directories and rebuild on every change.
# @parameter target [String] One of debug, mudge, beta, or release.
def watch(target: 'debug')
unless command_available?('inotifywait')
abort 'inotifywait is required (install inotify-tools).'
def watch(target: "debug")
unless command_available?("inotifywait")
abort "inotifywait is required (install inotify-tools)."
end
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}"
sleep 2
run_build_target(target)
@ -121,29 +121,29 @@ end
# Publish to beta/staging server
def publish_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
# Publish to production server
def publish
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
# Clean generated files
def clean
FileUtils.rm_rf('www')
puts 'Cleaned www/ directory'
FileUtils.rm_rf("www")
puts "Cleaned www/ directory"
end
# Run RSpec tests
def test
exec 'bundle exec rspec'
exec "bundle exec rspec"
end
# Run Guard for continuous testing
def guard
exec 'bundle exec guard'
exec "bundle exec guard"
end
# List all available drafts
@ -160,7 +160,7 @@ end
# Auto-fix StandardRB issues
def lint_fix
exec(*standardrb_command('--fix'))
exec(*standardrb_command("--fix"))
end
private
@ -168,26 +168,26 @@ private
# Build the site with specified URL
# @parameter url [String] The site URL to use
def build(url)
require_relative 'lib/pressa'
require_relative "lib/pressa"
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.generate(source_path: '.', target_path: 'www')
puts 'Site built successfully in www/'
generator.generate(source_path: ".", target_path: "www")
puts "Site built successfully in www/"
end
def run_build_target(target)
target_name = target.to_s
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
public_send(target_name)
end
def watch_paths
WATCHABLE_DIRECTORIES.flat_map { |path| ['-r', path] }
WATCHABLE_DIRECTORIES.flat_map { |path| ["-r", path] }
end
def standardrb_command(*extra_args)
@ -195,17 +195,17 @@ def standardrb_command(*extra_args)
end
def run_rsync(local_paths:, publish_dir:, dry_run:, delete:)
command = ['rsync', '-aKv', '-e', 'ssh -4']
command << '--dry-run' if dry_run
command << '--delete' if delete
command = ["rsync", "-aKv", "-e", "ssh -4"]
command << "--dry-run" if dry_run
command << "--delete" if delete
command.concat(local_paths)
command << "#{PUBLISH_HOST}:#{publish_dir}"
abort 'Error: rsync failed.' unless system(*command)
abort "Error: rsync failed." unless system(*command)
end
def resolve_draft_input(input_path)
if input_path.include?('/')
if input_path.start_with?('posts/')
if input_path.include?("/")
if input_path.start_with?("posts/")
abort "Error: '#{input_path}' is already published in posts/ directory"
end
@ -221,16 +221,15 @@ end
def slugify(title)
title.downcase
.gsub(/[^a-z0-9\s-]/, '')
.gsub(/\s+/, '-')
.gsub(/-+/, '-')
.gsub(/^-|-$/, '')
.gsub(/[^a-z0-9\s-]/, "")
.gsub(/\s+/, "-").squeeze("-")
.gsub(/^-|-$/, "")
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))
name_without_ext = File.basename(base_filename, '.md')
name_without_ext = File.basename(base_filename, ".md")
counter = 1
loop do
numbered_filename = "#{name_without_ext}-#{counter}.md"
@ -247,7 +246,7 @@ def render_draft_template(title)
Author: #{current_author}
Title: #{title}
Date: unpublished
Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}
Timestamp: #{now.strftime("%Y-%m-%dT%H:%M:%S%:z")}
Tags:
---
@ -258,27 +257,27 @@ def render_draft_template(title)
end
def current_author
Etc.getlogin || ENV['USER'] || `whoami`.strip
rescue StandardError
ENV['USER'] || `whoami`.strip
Etc.getlogin || ENV["USER"] || `whoami`.strip
rescue
ENV["USER"] || `whoami`.strip
end
def ordinal_date(time)
day = time.day
suffix = case day
when 1, 21, 31
'st'
"st"
when 2, 22
'nd'
"nd"
when 3, 23
'rd'
"rd"
else
'th'
"th"
end
time.strftime("#{day}#{suffix} %B, %Y")
end
def command_available?(command)
system('which', command, out: File::NULL, err: File::NULL)
system("which", command, out: File::NULL, err: File::NULL)
end

View file

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

View file

@ -1,4 +1,4 @@
require 'json'
require "json"
module Pressa
module Config
@ -89,7 +89,7 @@ module Pressa
keys.each do |key|
cursor[key] ||= {}
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
cursor = cursor[key]
@ -99,7 +99,7 @@ module Pressa
end
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/ }
raise ParseError, "Invalid table path '#{raw_path}' at line #{line_number}"
end
@ -107,7 +107,7 @@ module Pressa
end
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
key = source[0...separator].strip
@ -136,7 +136,7 @@ module Pressa
if in_string
if escaped
escaped = false
elsif char == '\\'
elsif char == "\\"
escaped = true
elsif char == '"'
in_string = false
@ -148,9 +148,9 @@ module Pressa
case char
when '"'
in_string = true
when '[', '{'
when "[", "{"
depth += 1
when ']', '}'
when "]", "}"
depth -= 1
end
end
@ -159,7 +159,7 @@ module Pressa
end
def strip_comments(line)
output = +''
output = +""
in_string = false
escaped = false
@ -169,7 +169,7 @@ module Pressa
if escaped
escaped = false
elsif char == '\\'
elsif char == "\\"
escaped = true
elsif char == '"'
in_string = false
@ -182,7 +182,7 @@ module Pressa
when '"'
in_string = true
output << char
when '#'
when "#"
break
else
output << char
@ -200,7 +200,7 @@ module Pressa
if in_string
if escaped
escaped = false
elsif char == '\\'
elsif char == "\\"
escaped = true
elsif char == '"'
in_string = false

View file

@ -1,6 +1,6 @@
require 'json'
require_relative '../utils/file_writer'
require_relative '../views/feed_post_view'
require "json"
require_relative "../utils/file_writer"
require_relative "../views/feed_post_view"
module Pressa
module Posts
@ -18,7 +18,7 @@ module Pressa
feed = build_feed(recent)
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)
end
@ -41,18 +41,18 @@ module Pressa
author:,
version: FEED_VERSION,
authors: [author],
feed_url: @site.url_for('/feed.json'),
language: 'en-CA',
feed_url: @site.url_for("/feed.json"),
language: "en-CA",
title: @site.title
}
end
def icon_url
@site.url_for('/images/apple-touch-icon-300.png')
@site.url_for("/images/apple-touch-icon-300.png")
end
def favicon_url
@site.url_for('/images/apple-touch-icon-80.png')
@site.url_for("/images/apple-touch-icon-80.png")
end
def feed_item(post)
@ -65,7 +65,7 @@ module Pressa
item[:tags] = post.tags unless post.tags.empty?
item[:content_html] = content_html
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[:id] = permalink

View file

@ -1,5 +1,5 @@
require 'yaml'
require 'date'
require "yaml"
require "date"
module Pressa
module Posts
@ -28,36 +28,36 @@ module Pressa
def validate_required_fields!
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
def parse_fields
@title = @raw['Title']
@author = @raw['Author']
timestamp = @raw['Timestamp']
@title = @raw["Title"]
@author = @raw["Author"]
timestamp = @raw["Timestamp"]
@date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime
@formatted_date = @raw['Date']
@link = @raw['Link']
@tags = parse_tags(@raw['Tags'])
@scripts = parse_scripts(@raw['Scripts'])
@styles = parse_styles(@raw['Styles'])
@formatted_date = @raw["Date"]
@link = @raw["Link"]
@tags = parse_tags(@raw["Tags"])
@scripts = parse_scripts(@raw["Scripts"])
@styles = parse_styles(@raw["Styles"])
end
def parse_tags(value)
return [] if value.nil?
value.is_a?(Array) ? value : value.split(',').map(&:strip)
value.is_a?(Array) ? value : value.split(",").map(&:strip)
end
def parse_comma_separated(value)
return [] if value.nil? || value.empty?
value.split(',').map(&:strip)
value.split(",").map(&:strip)
end
def parse_scripts(value)
return [] if value.nil?
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
@ -65,13 +65,13 @@ module Pressa
return [] if value.nil?
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
def normalize_asset_path(path, default_dir)
return path if path.start_with?('http://', 'https://', '/')
return path if path.include?('/')
return path if path.start_with?("http://", "https://", "/")
return path if path.include?("/")
"#{default_dir}/#{path}"
end

View file

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

View file

@ -1,8 +1,8 @@
require_relative '../plugin'
require_relative 'repo'
require_relative 'writer'
require_relative 'json_feed'
require_relative 'rss_feed'
require_relative "../plugin"
require_relative "repo"
require_relative "writer"
require_relative "json_feed"
require_relative "rss_feed"
module Pressa
module Posts
@ -10,7 +10,7 @@ module Pressa
attr_reader :posts_by_year
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)
repo = PostRepo.new

View file

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

View file

@ -1,6 +1,6 @@
require 'builder'
require_relative '../utils/file_writer'
require_relative '../views/feed_post_view'
require "builder"
require_relative "../utils/file_writer"
require_relative "../views/feed_post_view"
module Pressa
module Posts
@ -16,15 +16,15 @@ module Pressa
xml = Builder::XmlMarkup.new(indent: 2)
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
xml.rss version: "2.0",
"xmlns:atom" => "http://www.w3.org/2005/Atom",
"xmlns:content" => "http://purl.org/rss/1.0/modules/content/" do
xml.rss :version => "2.0",
"xmlns:atom" => "http://www.w3.org/2005/Atom",
"xmlns:content" => "http://purl.org/rss/1.0/modules/content/" do
xml.channel do
xml.title @site.title
xml.link @site.url
xml.description @site.description
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|
xml.item do
@ -35,13 +35,13 @@ module Pressa
xml.guid permalink, isPermaLink: "true"
xml.pubDate post.date.rfc822
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
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!)
end

View file

@ -1,10 +1,10 @@
require_relative '../utils/file_writer'
require_relative '../views/layout'
require_relative '../views/post_view'
require_relative '../views/recent_posts_view'
require_relative '../views/archive_view'
require_relative '../views/year_posts_view'
require_relative '../views/month_posts_view'
require_relative "../utils/file_writer"
require_relative "../views/layout"
require_relative "../views/post_view"
require_relative "../views/recent_posts_view"
require_relative "../views/archive_view"
require_relative "../views/year_posts_view"
require_relative "../views/month_posts_view"
module Pressa
module Posts
@ -28,11 +28,11 @@ module Pressa
page_subtitle: nil,
canonical_url: @site.url,
content: content_view,
page_description: 'Recent posts',
page_type: 'article'
page_description: "Recent posts",
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)
end
@ -40,13 +40,13 @@ module Pressa
content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site)
html = render_layout(
page_subtitle: 'Archive',
canonical_url: @site.url_for('/posts/'),
page_subtitle: "Archive",
canonical_url: @site.url_for("/posts/"),
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)
end
@ -68,7 +68,7 @@ module Pressa
private
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(
page_subtitle: post.title,
@ -77,10 +77,10 @@ module Pressa
page_scripts: post.scripts,
page_styles: post.styles,
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)
end
@ -92,10 +92,10 @@ module Pressa
canonical_url: @site.url_for("/posts/#{year}/"),
content: content_view,
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)
end
@ -109,10 +109,10 @@ module Pressa
canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"),
content: content_view,
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)
end
@ -123,7 +123,7 @@ module Pressa
page_scripts: [],
page_styles: [],
page_description: nil,
page_type: 'website'
page_type: "website"
)
layout = Views::Layout.new(
site: @site,

View file

@ -1,13 +1,13 @@
require_relative 'site'
require_relative 'site_generator'
require_relative 'plugin'
require_relative 'posts/plugin'
require_relative 'projects/plugin'
require_relative 'utils/markdown_renderer'
require_relative 'config/loader'
require_relative "site"
require_relative "site_generator"
require_relative "plugin"
require_relative "posts/plugin"
require_relative "projects/plugin"
require_relative "utils/markdown_renderer"
require_relative "config/loader"
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.build_site(url_override:)
end

View file

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

View file

@ -1,9 +1,9 @@
require_relative '../plugin'
require_relative '../utils/file_writer'
require_relative '../views/layout'
require_relative '../views/projects_view'
require_relative '../views/project_view'
require_relative 'models'
require_relative "../plugin"
require_relative "../utils/file_writer"
require_relative "../views/layout"
require_relative "../views/projects_view"
require_relative "../views/project_view"
require_relative "models"
module Pressa
module Projects
@ -34,12 +34,12 @@ module Pressa
html = render_layout(
site:,
page_subtitle: 'Projects',
canonical_url: site.url_for('/projects/'),
page_subtitle: "Projects",
canonical_url: site.url_for("/projects/"),
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)
end
@ -56,7 +56,7 @@ module Pressa
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)
end

View file

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

View file

@ -1,5 +1,5 @@
require 'fileutils'
require_relative 'utils/file_writer'
require "fileutils"
require_relative "utils/file_writer"
module Pressa
class SiteGenerator
@ -30,7 +30,7 @@ module Pressa
target_abs = absolute_path(target_path)
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
def absolute_path(path)
@ -42,12 +42,12 @@ module Pressa
end
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)
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.basename(source_file) == '.' || File.basename(source_file) == '..'
next if File.basename(source_file) == "." || File.basename(source_file) == ".."
filename = File.basename(source_file)
ext = File.extname(source_file)[1..]
@ -56,7 +56,7 @@ module Pressa
next
end
relative_path = source_file.sub("#{public_dir}/", '')
relative_path = source_file.sub("#{public_dir}/", "")
target_file = File.join(target_path, relative_path)
FileUtils.mkdir_p(File.dirname(target_file))
@ -69,11 +69,11 @@ module Pressa
end
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)
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)
filename = File.basename(source_file)
@ -82,10 +82,10 @@ module Pressa
if renderer.can_render_file?(filename:, extension: ext)
dir_name = File.dirname(source_file)
relative_path = if dir_name == public_dir
''
else
dir_name.sub("#{public_dir}/", '')
end
""
else
dir_name.sub("#{public_dir}/", "")
end
target_dir = File.join(target_path, relative_path)
renderer.render(site:, file_path: source_file, target_dir:)

View file

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

View file

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

View file

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

View file

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

View file

@ -4,19 +4,19 @@ module Pressa
module_function
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
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
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
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
private_class_method def svg(class_name:, view_box:, path:)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,39 +1,39 @@
require 'spec_helper'
require 'fileutils'
require 'tmpdir'
require "spec_helper"
require "fileutils"
require "tmpdir"
RSpec.describe Pressa::Config::Loader do
describe '#build_site' do
it 'builds a site from site.toml and projects.toml' do
describe "#build_site" do
it "builds a site from site.toml and projects.toml" do
with_temp_config do |dir|
loader = described_class.new(source_path: dir)
site = loader.build_site
expect(site.author).to eq('Sami Samhuri')
expect(site.url).to eq('https://samhuri.net')
expect(site.image_url).to eq('https://samhuri.net/images/me.jpg')
expect(site.styles.map(&:href)).to eq(['css/style.css'])
expect(site.author).to eq("Sami Samhuri")
expect(site.url).to eq("https://samhuri.net")
expect(site.image_url).to eq("https://samhuri.net/images/me.jpg")
expect(site.styles.map(&:href)).to eq(["css/style.css"])
projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) }
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
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|
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.image_url).to eq('https://beta.samhuri.net/images/me.jpg')
expect(site.url).to eq("https://beta.samhuri.net")
expect(site.image_url).to eq("https://beta.samhuri.net/images/me.jpg")
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|
File.write(File.join(dir, 'site.toml'), "title = \"x\"\n")
File.write(File.join(dir, 'projects.toml'), '')
File.write(File.join(dir, "site.toml"), "title = \"x\"\n")
File.write(File.join(dir, "projects.toml"), "")
loader = described_class.new(source_path: dir)
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
Dir.mktmpdir do |dir|
File.write(File.join(dir, 'site.toml'), <<~TOML)
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
@ -58,7 +58,7 @@ RSpec.describe Pressa::Config::Loader do
styles = []
TOML
File.write(File.join(dir, 'projects.toml'), <<~TOML)
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"

View file

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

View file

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

View file

@ -1,14 +1,14 @@
require 'spec_helper'
require 'fileutils'
require 'tmpdir'
require "spec_helper"
require "fileutils"
require "tmpdir"
RSpec.describe Pressa::Posts::PostRepo do
let(:repo) { described_class.new }
describe '#read_posts' do
it 'reads and organizes posts by year and month' do
describe "#read_posts" do
it "reads and organizes posts by year and month" do
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)
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.
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)
post = posts_by_year.all_posts.first
expect(post.title).to eq('Shredding in November')
expect(post.author).to eq('Shaun White')
expect(post.slug).to eq('shredding')
expect(post.title).to eq("Shredding in November")
expect(post.author).to eq("Shaun White")
expect(post.slug).to eq("shredding")
expect(post.year).to eq(2025)
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
it 'generates excerpts from post content' do
it "generates excerpts from post content" do
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)
post_content = <<~MARKDOWN
@ -58,15 +58,15 @@ RSpec.describe Pressa::Posts::PostRepo do
More content with a [link](https://example.net).
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
expect(post.excerpt).to include('test post')
expect(post.excerpt).not_to include('![')
expect(post.excerpt).to include('link')
expect(post.excerpt).not_to include('[link]')
expect(post.excerpt).to include("test post")
expect(post.excerpt).not_to include("![")
expect(post.excerpt).to include("link")
expect(post.excerpt).not_to include("[link]")
end
end
end

View file

@ -1,25 +1,25 @@
require 'spec_helper'
require 'fileutils'
require 'tmpdir'
require "spec_helper"
require "fileutils"
require "tmpdir"
RSpec.describe Pressa::SiteGenerator do
let(:site) do
Pressa::Site.new(
author: 'Sami Samhuri',
email: 'sami@samhuri.net',
title: 'samhuri.net',
description: 'blog',
url: 'https://samhuri.net',
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net",
plugins: [],
renderers: []
)
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|
FileUtils.mkdir_p(File.join(dir, 'public'))
source_file = File.join(dir, 'public', 'keep.txt')
File.write(source_file, 'safe')
FileUtils.mkdir_p(File.join(dir, "public"))
source_file = File.join(dir, "public", "keep.txt")
File.write(source_file, "safe")
generator = described_class.new(site:)
@ -27,7 +27,7 @@ RSpec.describe Pressa::SiteGenerator do
generator.generate(source_path: dir, target_path: dir)
}.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

View file

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

View file

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