diff --git a/pressa/Gemfile b/pressa/Gemfile
index 8bcdf50..2a0c9c4 100644
--- a/pressa/Gemfile
+++ b/pressa/Gemfile
@@ -9,6 +9,7 @@ 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'
diff --git a/pressa/Gemfile.lock b/pressa/Gemfile.lock
index 2846ec6..5631ad3 100644
--- a/pressa/Gemfile.lock
+++ b/pressa/Gemfile.lock
@@ -85,7 +85,27 @@ GEM
lumberjack (1.4.2)
mapping (1.1.3)
method_source (1.1.0)
+ mini_portile2 (2.8.9)
nenv (0.3.0)
+ nokogiri (1.18.10)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ nokogiri (1.18.10-aarch64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.10-aarch64-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.18.10-arm-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.10-arm-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.18.10-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.10-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.10-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.10-x86_64-linux-musl)
+ racc (~> 1.4)
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
@@ -185,6 +205,7 @@ DEPENDENCIES
guard-rspec (~> 4.7)
kramdown (~> 2.5)
kramdown-parser-gfm (~> 1.1)
+ nokogiri (~> 1.18)
phlex (~> 2.3)
rouge (~> 4.6)
rspec (~> 3.13)
diff --git a/pressa/bake.rb b/pressa/bake.rb
index ce4a3a0..edd8e77 100644
--- a/pressa/bake.rb
+++ b/pressa/bake.rb
@@ -87,6 +87,6 @@ def build(url)
puts "Building site for #{url}..."
site = Pressa.create_site(url_override: url)
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/"
end
diff --git a/pressa/bin/convert-all-posts b/pressa/bin/convert-all-posts
new file mode 100755
index 0000000..4d71a5b
--- /dev/null
+++ b/pressa/bin/convert-all-posts
@@ -0,0 +1,66 @@
+#!/usr/bin/env ruby
+
+require_relative '../lib/utils/frontmatter_converter'
+
+source_dir = ARGV[0] || '../posts'
+dry_run = ARGV.include?('--dry-run')
+
+unless Dir.exist?(source_dir)
+ puts "ERROR: Directory not found: #{source_dir}"
+ exit 1
+end
+
+posts = Dir.glob(File.join(source_dir, '**', '*.md')).sort
+
+puts "Found #{posts.length} posts to convert"
+puts "Mode: #{dry_run ? 'DRY RUN (no changes will be made)' : 'CONVERTING FILES'}"
+puts ""
+
+converted = 0
+errors = []
+
+posts.each do |post_path|
+ relative_path = post_path.sub("#{source_dir}/", '')
+
+ begin
+ if dry_run
+ content = File.read(post_path)
+ Pressa::Utils::FrontmatterConverter.convert_content(content)
+ puts "✓ Would convert: #{relative_path}"
+ else
+ Pressa::Utils::FrontmatterConverter.convert_file(post_path)
+ puts "✓ Converted: #{relative_path}"
+ end
+ converted += 1
+ rescue => e
+ errors << {path: relative_path, error: e.message}
+ puts "✗ Error: #{relative_path}"
+ puts " #{e.message}"
+ end
+end
+
+puts ""
+puts "=" * 60
+puts "CONVERSION SUMMARY"
+puts "=" * 60
+puts ""
+puts "Total posts: #{posts.length}"
+puts "Converted: #{converted}"
+puts "Errors: #{errors.length}"
+puts ""
+
+if errors.any?
+ puts "Posts with errors:"
+ errors.each do |err|
+ puts " #{err[:path]}"
+ puts " #{err[:error]}"
+ end
+ puts ""
+end
+
+if dry_run
+ puts "This was a dry run. Run without --dry-run to actually convert files."
+ puts ""
+end
+
+exit(errors.empty? ? 0 : 1)
diff --git a/pressa/bin/validate-output b/pressa/bin/validate-output
index fb8c913..cb39e3c 100755
--- a/pressa/bin/validate-output
+++ b/pressa/bin/validate-output
@@ -1,16 +1,20 @@
#!/usr/bin/env ruby
+require 'optparse'
require 'fileutils'
require 'digest'
class OutputValidator
- def initialize(swift_dir:, ruby_dir:)
+ def initialize(swift_dir:, ruby_dir:, verbose:, ignore_patterns:, show_details:)
@swift_dir = swift_dir
@ruby_dir = ruby_dir
@differences = []
@missing_in_ruby = []
@missing_in_swift = []
@identical_count = 0
+ @verbose = verbose
+ @ignore_patterns = ignore_patterns
+ @show_details = show_details
end
def validate
@@ -22,8 +26,8 @@ class OutputValidator
swift_files = find_html_files(@swift_dir)
ruby_files = find_html_files(@ruby_dir)
- puts "Found #{swift_files.length} Swift HTML files"
- puts "Found #{ruby_files.length} Ruby HTML files"
+ puts "Found #{swift_files.length} Swift output files"
+ puts "Found #{ruby_files.length} Ruby output files"
puts ""
compare_files(swift_files, ruby_files)
@@ -33,7 +37,7 @@ class OutputValidator
private
def find_html_files(dir)
- Dir.glob(File.join(dir, '**', '*.{html,xml,json}'))
+ Dir.glob(File.join(dir, '**', '*.{html,xml,json,css,js,png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,eot}'))
.map { |f| f.sub("#{dir}/", '') }
.sort
end
@@ -42,6 +46,8 @@ class OutputValidator
all_files = (swift_files + ruby_files).uniq.sort
all_files.each do |relative_path|
+ next if ignored?(relative_path)
+
swift_path = File.join(@swift_dir, relative_path)
ruby_path = File.join(@ruby_dir, relative_path)
@@ -56,12 +62,17 @@ class OutputValidator
end
def compare_file_contents(relative_path, swift_path, ruby_path)
- swift_content = normalize_html(File.read(swift_path))
- ruby_content = normalize_html(File.read(ruby_path))
+ if binary_file?(relative_path)
+ swift_content = File.binread(swift_path)
+ ruby_content = File.binread(ruby_path)
+ else
+ swift_content = normalize_html(File.read(swift_path))
+ ruby_content = normalize_html(File.read(ruby_path))
+ end
if swift_content == ruby_content
@identical_count += 1
- puts "✓ #{relative_path}"
+ puts "✓ #{relative_path}" if @verbose
else
@differences << {
path: relative_path,
@@ -74,6 +85,17 @@ class OutputValidator
end
end
+ def ignored?(path)
+ return false if @ignore_patterns.empty?
+
+ @ignore_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
+ end
+
+ def binary_file?(path)
+ ext = File.extname(path).downcase
+ %w[.png .jpg .jpeg .gif .svg .ico .woff .woff2 .ttf .eot].include?(ext)
+ end
+
def normalize_html(content)
content
.gsub(/\s+/, ' ')
@@ -96,6 +118,8 @@ class OutputValidator
if @differences.any?
puts "Files with differences:"
+ return unless @show_details
+
@differences.each do |diff|
puts " #{diff[:path]}"
puts " Swift: #{diff[:swift_size]} bytes (#{diff[:swift_hash][0..7]}...)"
@@ -124,13 +148,32 @@ class OutputValidator
end
end
+options = {
+ verbose: false,
+ ignore_patterns: [],
+ show_details: false
+}
+
+parser = OptionParser.new do |opts|
+ opts.banner = "Usage: validate-output [options] SWIFT_DIR RUBY_DIR"
+
+ opts.on('-v', '--verbose', 'Print a ✓ line for every identical file') do
+ options[:verbose] = true
+ end
+
+ opts.on('-iPATTERN', '--ignore=PATTERN', 'Ignore files matching the glob (may be repeated)') do |pattern|
+ options[:ignore_patterns] << pattern
+ end
+
+ opts.on('--details', 'Include byte counts and hashes for differing files') do
+ options[:show_details] = true
+ end
+end
+
+parser.parse!
+
if ARGV.length != 2
- puts "Usage: validate-output SWIFT_DIR RUBY_DIR"
- puts ""
- puts "Compares HTML/XML/JSON output from Swift and Ruby generators."
- puts ""
- puts "Example:"
- puts " validate-output www-swift www-ruby"
+ puts parser
exit 1
end
@@ -147,5 +190,11 @@ unless Dir.exist?(ruby_dir)
exit 1
end
-validator = OutputValidator.new(swift_dir: swift_dir, ruby_dir: ruby_dir)
+validator = OutputValidator.new(
+ swift_dir: swift_dir,
+ ruby_dir: ruby_dir,
+ verbose: options[:verbose],
+ ignore_patterns: options[:ignore_patterns],
+ show_details: options[:show_details]
+)
validator.validate
diff --git a/pressa/lib/posts/json_feed.rb b/pressa/lib/posts/json_feed.rb
index 303b3d2..920c49e 100644
--- a/pressa/lib/posts/json_feed.rb
+++ b/pressa/lib/posts/json_feed.rb
@@ -1,5 +1,6 @@
require 'json'
require_relative '../utils/file_writer'
+require_relative '../views/feed_post_view'
module Pressa
module Posts
@@ -14,20 +15,7 @@ module Pressa
def write_feed(target_path:, limit: 30)
recent = @posts_by_year.recent_posts(limit)
- feed = {
- version: FEED_VERSION,
- title: @site.title,
- home_page_url: @site.url,
- feed_url: @site.url_for('/feed.json'),
- description: @site.description,
- authors: [
- {
- name: @site.author,
- url: @site.url
- }
- ],
- items: recent.map { |post| feed_item(post) }
- }
+ feed = build_feed(recent)
json = JSON.pretty_generate(feed)
file_path = File.join(target_path, 'feed.json')
@@ -36,22 +24,49 @@ module Pressa
private
- def feed_item(post)
- item = {
- id: @site.url_for(post.path),
- url: post.link_post? ? post.link : @site.url_for(post.path),
- title: post.link_post? ? "→ #{post.title}" : post.title,
- content_html: post.body,
- summary: post.excerpt,
- date_published: post.date.iso8601,
- authors: [
- {
- name: post.author
- }
- ]
+ def build_feed(posts)
+ author = {
+ name: @site.author,
+ url: @site.url,
+ avatar: @site.image_url
}
+ items = posts.map { |post| feed_item(post) }
+
+ {
+ icon: icon_url,
+ favicon: favicon_url,
+ items: items,
+ home_page_url: @site.url,
+ author:,
+ version: FEED_VERSION,
+ authors: [author],
+ 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')
+ end
+
+ def favicon_url
+ @site.url_for('/images/apple-touch-icon-80.png')
+ end
+
+ def feed_item(post)
+ content_html = Views::FeedPostView.new(post:, site: @site).call
+ permalink = @site.url_for(post.path)
+
+ item = {}
+ item[:url] = post.link_post? ? post.link : permalink
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[:date_published] = post.date.iso8601
+ item[:id] = permalink
item
end
diff --git a/pressa/lib/posts/metadata.rb b/pressa/lib/posts/metadata.rb
index 373bd58..5c972cd 100644
--- a/pressa/lib/posts/metadata.rb
+++ b/pressa/lib/posts/metadata.rb
@@ -38,11 +38,16 @@ module Pressa
@date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime
@formatted_date = @raw['Date']
@link = @raw['Link']
- @tags = parse_comma_separated(@raw['Tags'])
+ @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)
+ end
+
def parse_comma_separated(value)
return [] if value.nil? || value.empty?
value.split(',').map(&:strip)
@@ -50,12 +55,25 @@ module Pressa
def parse_scripts(value)
return [] if value.nil?
- parse_comma_separated(value).map { |src| Script.new(src:, defer: true) }
+
+ parse_comma_separated(value).map do |src|
+ Script.new(src: normalize_asset_path(src, 'js'), defer: true)
+ end
end
def parse_styles(value)
return [] if value.nil?
- parse_comma_separated(value).map { |href| Stylesheet.new(href:) }
+
+ parse_comma_separated(value).map do |href|
+ 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?('/')
+
+ "#{default_dir}/#{path}"
end
end
end
diff --git a/pressa/lib/posts/repo.rb b/pressa/lib/posts/repo.rb
index a1cf4c3..4ecf543 100644
--- a/pressa/lib/posts/repo.rb
+++ b/pressa/lib/posts/repo.rb
@@ -76,20 +76,22 @@ module Pressa
def generate_excerpt(markdown)
text = markdown.dup
- text.gsub!(/!\[.*?\]\([^)]+\)/, '')
+ text.gsub!(/!\[[^\]]*\]\([^)]+\)/, '')
+ text.gsub!(/!\[[^\]]*\]\[[^\]]+\]/, '')
- text.gsub!(/\[(.*?)\]\([^)]+\)/, '\1')
+ text.gsub!(/\[([^\]]+)\]\([^)]+\)/, '\1')
+ text.gsub!(/\[([^\]]+)\]\[[^\]]+\]/, '\1')
+
+ text.gsub!(/(?m)^\[[^\]]+\]:\s*\S.*$/, '')
text.gsub!(/<[^>]+>/, '')
text.gsub!(/\s+/, ' ')
text.strip!
- if text.length > EXCERPT_LENGTH
- "#{text[0...EXCERPT_LENGTH]}..."
- else
- text
- end
+ return '...' if text.empty?
+
+ "#{text[0...EXCERPT_LENGTH]}..."
end
def add_post_to_hierarchy(post)
diff --git a/pressa/lib/posts/rss_feed.rb b/pressa/lib/posts/rss_feed.rb
index c723903..32f3dad 100644
--- a/pressa/lib/posts/rss_feed.rb
+++ b/pressa/lib/posts/rss_feed.rb
@@ -1,5 +1,6 @@
require 'builder'
require_relative '../utils/file_writer'
+require_relative '../views/feed_post_view'
module Pressa
module Posts
@@ -15,29 +16,26 @@ 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" 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.language "en-us"
xml.pubDate recent.first.date.rfc822 if recent.any?
- xml.lastBuildDate Time.now.rfc822
xml.tag! "atom:link", href: @site.url_for('/feed.xml'), rel: "self", type: "application/rss+xml"
recent.each do |post|
xml.item do
title = post.link_post? ? "→ #{post.title}" : post.title
+ permalink = @site.url_for(post.path)
xml.title title
- xml.link post.link_post? ? post.link : @site.url_for(post.path)
- xml.guid @site.url_for(post.path), isPermaLink: "true"
- xml.description { xml.cdata! post.body }
+ xml.link permalink
+ xml.guid permalink, isPermaLink: "true"
xml.pubDate post.date.rfc822
- xml.author "#{@site.email} (#{@site.author})"
-
- post.tags.each do |tag|
- xml.category tag
- end
+ xml.author post.author
+ xml.tag!('content:encoded') { xml.cdata!(render_feed_post(post)) }
end
end
end
@@ -46,6 +44,10 @@ module Pressa
file_path = File.join(target_path, 'feed.xml')
Utils::FileWriter.write(path: file_path, content: xml.target!)
end
+
+ def render_feed_post(post)
+ Views::FeedPostView.new(post:, site: @site).call
+ end
end
end
end
diff --git a/pressa/lib/posts/writer.rb b/pressa/lib/posts/writer.rb
index 800887c..70c9f92 100644
--- a/pressa/lib/posts/writer.rb
+++ b/pressa/lib/posts/writer.rb
@@ -25,9 +25,11 @@ module Pressa
content_view = Views::RecentPostsView.new(posts: recent, site: @site)
html = render_layout(
- page_title: @site.title,
+ page_subtitle: nil,
canonical_url: @site.url,
- content: content_view
+ content: content_view,
+ page_description: 'Recent posts',
+ page_type: 'article'
)
file_path = File.join(target_path, 'index.html')
@@ -38,9 +40,10 @@ module Pressa
content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site)
html = render_layout(
- page_title: "Archive – #{@site.title}",
+ page_subtitle: 'Archive',
canonical_url: @site.url_for('/posts/'),
- content: content_view
+ content: content_view,
+ page_description: 'Archive of all posts'
)
file_path = File.join(target_path, 'posts', 'index.html')
@@ -65,17 +68,16 @@ module Pressa
private
def write_post(post:, target_path:)
- content_view = Views::PostView.new(post:, site: @site)
-
- all_scripts = @site.scripts + post.scripts
- all_styles = @site.styles + post.styles
+ content_view = Views::PostView.new(post:, site: @site, article_class: 'container')
html = render_layout(
- page_title: "#{post.title} – #{@site.title}",
+ page_subtitle: post.title,
canonical_url: @site.url_for(post.path),
content: content_view,
page_scripts: post.scripts,
- page_styles: post.styles
+ page_styles: post.styles,
+ page_description: post.excerpt,
+ page_type: 'article'
)
file_path = File.join(target_path, post.path.sub(/^\//, ''), 'index.html')
@@ -86,9 +88,11 @@ module Pressa
content_view = Views::YearPostsView.new(year:, year_posts:, site: @site)
html = render_layout(
- page_title: "#{year} – #{@site.title}",
+ page_subtitle: year.to_s,
canonical_url: @site.url_for("/posts/#{year}/"),
- content: content_view
+ content: content_view,
+ page_description: "Archive of all posts from #{year}",
+ page_type: 'article'
)
file_path = File.join(target_path, 'posts', year.to_s, 'index.html')
@@ -101,22 +105,34 @@ module Pressa
title = "#{month.name} #{year}"
html = render_layout(
- page_title: "#{title} – #{@site.title}",
+ page_subtitle: title,
canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"),
- content: content_view
+ content: content_view,
+ page_description: "Archive of all posts from #{title}",
+ page_type: 'article'
)
file_path = File.join(target_path, 'posts', year.to_s, month.padded, 'index.html')
Utils::FileWriter.write(path: file_path, content: html)
end
- def render_layout(page_title:, canonical_url:, content:, page_scripts: [], page_styles: [])
+ def render_layout(
+ page_subtitle:,
+ canonical_url:,
+ content:,
+ page_scripts: [],
+ page_styles: [],
+ page_description: nil,
+ page_type: 'website'
+ )
layout = Views::Layout.new(
site: @site,
- page_title:,
+ page_subtitle:,
canonical_url:,
page_scripts:,
- page_styles:
+ page_styles:,
+ page_description:,
+ page_type:
)
layout.call do
diff --git a/pressa/lib/pressa.rb b/pressa/lib/pressa.rb
index 39d9a12..c1fcdd8 100644
--- a/pressa/lib/pressa.rb
+++ b/pressa/lib/pressa.rb
@@ -9,47 +9,57 @@ module Pressa
def self.create_site(url_override: nil)
url = url_override || 'https://samhuri.net'
- projects = [
+ build_project = lambda do |name, title, description|
Projects::Project.new(
- name: 'bin',
- title: 'bin',
- description: 'my collection of scripts in ~/bin',
- url: 'https://github.com/samsonjs/bin'
- ),
- Projects::Project.new(
- name: 'config',
- title: 'config',
- description: 'important dot files',
- url: 'https://github.com/samsonjs/config'
- ),
- Projects::Project.new(
- name: 'unix-node',
- title: 'unix-node',
- description: 'Node.js CommonJS module that exports useful Unix commands',
- url: 'https://github.com/samsonjs/unix-node'
- ),
- Projects::Project.new(
- name: 'strftime',
- title: 'strftime',
- description: 'strftime for JavaScript',
- url: 'https://github.com/samsonjs/strftime'
+ name:,
+ title:,
+ description:,
+ url: "https://github.com/samsonjs/#{title}"
)
+ end
+
+ projects = [
+ build_project.call('bin', 'bin', 'my collection of scripts in ~/bin'),
+ build_project.call('config', 'config', 'important dot files (zsh, emacs, vim, screen)'),
+ build_project.call('compiler', 'compiler', 'a compiler targeting x86 in Ruby'),
+ build_project.call('lake', 'lake', 'a simple implementation of Scheme in C'),
+ build_project.call('strftime', 'strftime', 'strftime for JavaScript'),
+ build_project.call('format', 'format', 'printf for JavaScript'),
+ build_project.call('gitter', 'gitter', 'a GitHub client for Node (v3 API)'),
+ build_project.call('mojo.el', 'mojo.el', 'turn emacs into a sweet mojo editor'),
+ build_project.call('ThePusher', 'ThePusher', 'Github post-receive hook router'),
+ build_project.call('NorthWatcher', 'NorthWatcher', 'cron for filesystem changes'),
+ build_project.call('repl-edit', 'repl-edit', 'edit Node repl commands with your text editor'),
+ build_project.call('cheat.el', 'cheat.el', 'cheat from emacs'),
+ build_project.call('batteries', 'batteries', 'a general purpose node library'),
+ build_project.call('samhuri.net', 'samhuri.net', 'this site')
+ ]
+
+ project_scripts = [
+ Script.new(src: 'https://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js', defer: true),
+ Script.new(src: 'js/gitter.js', defer: true),
+ Script.new(src: 'js/store.js', defer: true),
+ Script.new(src: 'js/projects.js', defer: true)
]
Site.new(
author: 'Sami Samhuri',
email: 'sami@samhuri.net',
title: 'samhuri.net',
- description: 'The personal blog of Sami Samhuri',
+ description: "Sami Samhuri's blog about programming, mainly about iOS and Ruby and Rails these days.",
url:,
- image_url: "#{url}/images",
+ image_url: "#{url}/images/me.jpg",
scripts: [],
styles: [
- Stylesheet.new(href: 'css/style.css')
+ Stylesheet.new(href: 'css/normalize.css'),
+ Stylesheet.new(href: 'css/style.css'),
+ Stylesheet.new(href: 'css/fontawesome.min.css'),
+ Stylesheet.new(href: 'css/brands.min.css'),
+ Stylesheet.new(href: 'css/solid.min.css')
],
plugins: [
Posts::Plugin.new,
- Projects::Plugin.new(projects:)
+ Projects::Plugin.new(projects:, scripts: project_scripts, styles: [])
],
renderers: [
Utils::MarkdownRenderer.new
diff --git a/pressa/lib/projects/plugin.rb b/pressa/lib/projects/plugin.rb
index 3f487bc..3c1650d 100644
--- a/pressa/lib/projects/plugin.rb
+++ b/pressa/lib/projects/plugin.rb
@@ -8,8 +8,12 @@ require_relative 'models'
module Pressa
module Projects
class Plugin < Pressa::Plugin
- def initialize(projects: [])
+ attr_reader :scripts, :styles
+
+ def initialize(projects: [], scripts: [], styles: [])
@projects = projects
+ @scripts = scripts
+ @styles = styles
end
def setup(site:, source_path:)
@@ -30,7 +34,7 @@ module Pressa
html = render_layout(
site:,
- page_title: "Projects – #{site.title}",
+ page_subtitle: 'Projects',
canonical_url: site.url_for('/projects/'),
content: content_view
)
@@ -44,20 +48,34 @@ module Pressa
html = render_layout(
site:,
- page_title: "#{project.title} – #{site.title}",
+ page_subtitle: project.title,
canonical_url: site.url_for(project.path),
- content: content_view
+ content: content_view,
+ page_scripts: @scripts,
+ page_styles: @styles,
+ page_description: project.description
)
file_path = File.join(target_path, 'projects', project.name, 'index.html')
Utils::FileWriter.write(path: file_path, content: html)
end
- def render_layout(site:, page_title:, canonical_url:, content:)
+ def render_layout(
+ site:,
+ page_subtitle:,
+ canonical_url:,
+ content:,
+ page_scripts: [],
+ page_styles: [],
+ page_description: nil
+ )
layout = Views::Layout.new(
site:,
- page_title:,
- canonical_url:
+ page_subtitle:,
+ canonical_url:,
+ page_scripts:,
+ page_styles:,
+ page_description:
)
layout.call do
diff --git a/pressa/lib/site_generator.rb b/pressa/lib/site_generator.rb
index 309c2dc..babc92d 100644
--- a/pressa/lib/site_generator.rb
+++ b/pressa/lib/site_generator.rb
@@ -31,6 +31,13 @@ module Pressa
next if File.directory?(source_file)
next if File.basename(source_file) == '.' || File.basename(source_file) == '..'
+ filename = File.basename(source_file)
+ ext = File.extname(source_file)[1..]
+
+ if can_render?(filename, ext)
+ next
+ end
+
relative_path = source_file.sub("#{public_dir}/", '')
target_file = File.join(target_path, relative_path)
@@ -39,6 +46,10 @@ module Pressa
end
end
+ def can_render?(filename, ext)
+ site.renderers.any? { |renderer| renderer.can_render_file?(filename:, extension: ext) }
+ end
+
def process_public_directory(source_path, target_path)
public_dir = File.join(source_path, 'public')
return unless Dir.exist?(public_dir)
@@ -51,7 +62,12 @@ module Pressa
ext = File.extname(source_file)[1..]
if renderer.can_render_file?(filename:, extension: ext)
- relative_path = File.dirname(source_file).sub("#{public_dir}/", '')
+ dir_name = File.dirname(source_file)
+ relative_path = if dir_name == public_dir
+ ''
+ else
+ dir_name.sub("#{public_dir}/", '')
+ end
target_dir = File.join(target_path, relative_path)
renderer.render(site:, file_path: source_file, target_dir:)
diff --git a/pressa/lib/utils/file_writer.rb b/pressa/lib/utils/file_writer.rb
index 1c2809b..4bd5444 100644
--- a/pressa/lib/utils/file_writer.rb
+++ b/pressa/lib/utils/file_writer.rb
@@ -1,4 +1,5 @@
require 'fileutils'
+require_relative 'html_formatter'
module Pressa
module Utils
@@ -6,7 +7,13 @@ module Pressa
def self.write(path:, content:, permissions: 0o644)
FileUtils.mkdir_p(File.dirname(path))
- File.write(path, content, mode: 'w')
+ formatted_content = if path.end_with?('.html')
+ HtmlFormatter.format(content)
+ else
+ content
+ end
+
+ File.write(path, formatted_content, mode: 'w')
File.chmod(permissions, path)
end
diff --git a/pressa/lib/utils/frontmatter_converter.rb b/pressa/lib/utils/frontmatter_converter.rb
index 65cad2b..2c1c766 100644
--- a/pressa/lib/utils/frontmatter_converter.rb
+++ b/pressa/lib/utils/frontmatter_converter.rb
@@ -1,7 +1,7 @@
module Pressa
module Utils
class FrontmatterConverter
- FIELD_PATTERN = /^([A-Z][a-z]+):\s*(.+)$/
+ FIELD_PATTERN = /^([A-Z][A-Za-z\s]+):\s*(.+)$/
def self.convert_file(input_path, output_path = nil)
content = File.read(input_path)
@@ -40,7 +40,7 @@ module Pressa
next if line.empty?
if line =~ FIELD_PATTERN
- field_name = $1
+ field_name = $1.strip
field_value = $2.strip
fields[field_name] = field_value
@@ -56,12 +56,27 @@ module Pressa
end
private_class_method def self.format_yaml_field(name, value)
- needs_quoting = (value.include?(':') && !value.start_with?('http')) ||
+ return "#{name}: #{value}" if name == 'Timestamp'
+
+ if name == 'Tags'
+ tags = value.split(',').map(&:strip)
+ return "#{name}: [#{tags.join(', ')}]"
+ end
+
+ if name == 'Title'
+ escaped_value = value.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
+ return "#{name}: \"#{escaped_value}\""
+ end
+
+ has_special_chars = value.include?('\\') || value.include?('"')
+ needs_quoting = has_special_chars ||
+ (value.include?(':') && !value.start_with?('http')) ||
value.include?(',') ||
name == 'Date'
if needs_quoting
- "#{name}: \"#{value}\""
+ escaped_value = value.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
+ "#{name}: \"#{escaped_value}\""
else
"#{name}: #{value}"
end
diff --git a/pressa/lib/utils/html_formatter.rb b/pressa/lib/utils/html_formatter.rb
new file mode 100644
index 0000000..af03d73
--- /dev/null
+++ b/pressa/lib/utils/html_formatter.rb
@@ -0,0 +1,91 @@
+module Pressa
+ module Utils
+ module HtmlFormatter
+ INDENT = 2
+ VOID_TAGS = %w[
+ area base br col embed hr img input link meta param source track wbr
+ ].freeze
+
+ PLACEHOLDER_PREFIX = '%%PRESSA_PRESERVE_'
+ PRESERVE_PATTERNS = [
+ /
.*?<\/div>/m,
+ /
.*?<\/div>/m
+ ].freeze
+
+ def self.format(html)
+ html_with_placeholders, preserved = preserve_sections(html)
+ formatted = format_with_indentation(html_with_placeholders)
+ restore_sections(formatted, preserved)
+ end
+
+ def self.format_with_indentation(html)
+ indent_level = 0
+
+ formatted_lines = split_lines(html).map do |line|
+ stripped = line.strip
+ next if stripped.empty?
+
+ decrease_indent = closing_tag?(stripped)
+ indent_level = [indent_level - INDENT, 0].max if decrease_indent
+
+ content = tag_line?(stripped) ? stripped : line
+ current_line = (' ' * indent_level) + content
+
+ if tag_line?(stripped) && !decrease_indent && !void_tag?(stripped) && !self_closing?(stripped)
+ indent_level += INDENT
+ end
+
+ current_line
+ end
+
+ formatted_lines.compact.join("\n")
+ end
+
+ def self.split_lines(html)
+ html.gsub(/>\s*, ">\n<").split("\n")
+ end
+
+ def self.tag_line?(line)
+ line.start_with?('<') && !line.start_with?('') || line.include?('')
+ end
+
+ def self.tag_name(line)
+ line[%r{\A?([^\s>/]+)}, 1]&.downcase
+ end
+
+ def self.void_tag?(line)
+ VOID_TAGS.include?(tag_name(line))
+ end
+
+ def self.preserve_sections(html)
+ preserved = {}
+ index = 0
+
+ PRESERVE_PATTERNS.each do |pattern|
+ html = html.gsub(pattern) do |match|
+ placeholder = "#{PLACEHOLDER_PREFIX}#{index}%%"
+ preserved[placeholder] = match
+ index += 1
+ placeholder
+ end
+ end
+
+ [html, preserved]
+ end
+
+ def self.restore_sections(html, preserved)
+ preserved.reduce(html) do |content, (placeholder, original)|
+ content.gsub(placeholder, original)
+ end
+ end
+ end
+ end
+end
diff --git a/pressa/lib/utils/markdown_renderer.rb b/pressa/lib/utils/markdown_renderer.rb
index 3ac7295..6897a56 100644
--- a/pressa/lib/utils/markdown_renderer.rb
+++ b/pressa/lib/utils/markdown_renderer.rb
@@ -1,6 +1,7 @@
require 'kramdown'
require 'yaml'
require_relative 'file_writer'
+require_relative '../site'
require_relative '../views/layout'
class String
@@ -10,6 +11,8 @@ end
module Pressa
module Utils
class MarkdownRenderer
+ EXCERPT_LENGTH = 300
+
def can_render_file?(filename:, extension:)
extension == 'md'
end
@@ -20,20 +23,35 @@ module Pressa
html_body = render_markdown(body_markdown)
- page_title = metadata['Title'] || File.basename(file_path, '.md').capitalize
- show_extension = metadata['Show extension'] == 'true'
+ 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')
+
+ 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
html = render_layout(
site:,
- page_title:,
- canonical_url: site.url,
- body: html_body
+ page_subtitle: page_title,
+ canonical_url: site.url_for(canonical_path),
+ body: html_body,
+ page_description:,
+ page_type:
)
output_filename = if show_extension
- File.basename(file_path, '.md') + '.html'
+ "#{slug}.html"
else
- File.join(File.basename(file_path, '.md'), 'index.html')
+ File.join(slug, 'index.html')
end
output_path = File.join(target_dir, output_filename)
@@ -65,21 +83,70 @@ module Pressa
).to_html
end
- def render_layout(site:, page_title:, canonical_url:, body:)
+ def render_layout(site:, page_subtitle:, canonical_url:, body:, page_description:, page_type:)
layout = Views::Layout.new(
site:,
- page_title: "#{page_title} – #{site.title}",
- canonical_url:
+ page_subtitle:,
+ canonical_url:,
+ page_description:,
+ page_type:
)
+ content_view = PageView.new(page_title: page_subtitle, body:)
layout.call do
- article(class: "page") do
- h1 { page_title }
- raw(body)
- div(class: "fin") { "◼" }
+ content_view.call
+ end
+ end
+
+ class PageView < Phlex::HTML
+ def initialize(page_title:, body:)
+ @page_title = page_title
+ @body = body
+ end
+
+ def view_template
+ article(class: 'container') do
+ h1 { @page_title }
+ raw(@body)
+ end
+
+ div(class: 'row clearfix') do
+ p(class: 'fin') do
+ i(class: 'fa fa-code')
+ end
end
end
end
+
+ def generate_excerpt(markdown)
+ text = markdown.dup
+
+ # Drop inline and reference-style images before links are simplified.
+ 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!(/<[^>]+>/, '')
+ text.gsub!(/\s+/, ' ')
+ text.strip!
+
+ return nil if text.empty?
+
+ "#{text[0...EXCERPT_LENGTH]}..."
+ end
+
+ def presence(value)
+ return value unless value.respond_to?(:strip)
+
+ stripped = value.strip
+ stripped.empty? ? nil : stripped
+ end
end
end
end
diff --git a/pressa/lib/views/archive_view.rb b/pressa/lib/views/archive_view.rb
index c9cdf17..6413a8a 100644
--- a/pressa/lib/views/archive_view.rb
+++ b/pressa/lib/views/archive_view.rb
@@ -1,4 +1,5 @@
require 'phlex'
+require_relative 'year_posts_view'
module Pressa
module Views
@@ -9,52 +10,13 @@ module Pressa
end
def view_template
- article(class: "archive") do
- h1 { "Archive" }
-
- @posts_by_year.sorted_years.each do |year|
- year_posts = @posts_by_year.by_year[year]
- render_year(year, year_posts)
- end
+ div(class: 'container') do
+ h1 { 'Archive' }
end
- end
- private
-
- def render_year(year, year_posts)
- section(class: "year") do
- h2 do
- a(href: @site.url_for("/posts/#{year}/")) { year.to_s }
- end
-
- year_posts.sorted_months.each do |month_posts|
- render_month(year, month_posts)
- end
- end
- end
-
- def render_month(year, month_posts)
- month = month_posts.month
-
- section(class: "month") do
- h3 do
- a(href: @site.url_for("/posts/#{year}/#{month.padded}/")) do
- "#{month.name} #{year}"
- end
- end
-
- ul do
- month_posts.sorted_posts.each do |post|
- li do
- if post.link_post?
- a(href: post.link) { "→ #{post.title}" }
- else
- a(href: @site.url_for(post.path)) { post.title }
- end
- plain " – #{post.formatted_date}"
- end
- end
- end
+ @posts_by_year.sorted_years.each do |year|
+ year_posts = @posts_by_year.by_year[year]
+ render Views::YearPostsView.new(year:, year_posts:, site: @site)
end
end
end
diff --git a/pressa/lib/views/feed_post_view.rb b/pressa/lib/views/feed_post_view.rb
new file mode 100644
index 0000000..57ba351
--- /dev/null
+++ b/pressa/lib/views/feed_post_view.rb
@@ -0,0 +1,24 @@
+require 'phlex'
+
+module Pressa
+ module Views
+ class FeedPostView < Phlex::HTML
+ def initialize(post:, site:)
+ @post = post
+ @site = site
+ end
+
+ def view_template
+ div do
+ p(class: 'time') { @post.formatted_date }
+ raw(@post.body)
+ p do
+ a(class: 'permalink', href: @site.url_for(@post.path)) { '∞' }
+ end
+ end
+ end
+
+ private
+ end
+ end
+end
diff --git a/pressa/lib/views/layout.rb b/pressa/lib/views/layout.rb
index 725db4e..ffffeba 100644
--- a/pressa/lib/views/layout.rb
+++ b/pressa/lib/views/layout.rb
@@ -3,52 +3,97 @@ require 'phlex'
module Pressa
module Views
class Layout < Phlex::HTML
- attr_reader :site, :page_title, :canonical_url, :page_scripts, :page_styles
+ START_YEAR = 2006
- def initialize(site:, page_title:, canonical_url:, page_scripts: [], page_styles: [])
+ attr_reader :site,
+ :page_subtitle,
+ :page_description,
+ :page_type,
+ :canonical_url,
+ :page_scripts,
+ :page_styles
+
+ def initialize(
+ site:,
+ page_subtitle: nil,
+ canonical_url:,
+ page_description: nil,
+ page_type: 'website',
+ page_scripts: [],
+ page_styles: []
+ )
@site = site
- @page_title = page_title
+ @page_subtitle = page_subtitle
+ @page_description = page_description
+ @page_type = page_type
@canonical_url = canonical_url
@page_scripts = page_scripts
@page_styles = page_styles
end
+ def format_output?
+ true
+ end
+
def view_template(&block)
doctype
- html(lang: "en") do
+ html(lang: 'en') do
+ comment { 'meow' }
+
head do
- meta(charset: "utf-8")
- meta(name: "viewport", content: "width=device-width, initial-scale=1")
- title { page_title }
- meta(name: "description", content: site.description)
- meta(name: "author", content: site.author)
+ 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)
- link(rel: "canonical", href: canonical_url)
+ 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')
- meta(property: "og:title", content: page_title)
- meta(property: "og:description", content: site.description)
- meta(property: "og:url", content: canonical_url)
- meta(property: "og:type", content: "website")
- if site.image_url
- meta(property: "og:image", content: site.image_url)
- end
+ link(
+ 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',
+ title: site.title
+ )
- meta(name: "twitter:card", content: "summary")
- meta(name: "twitter:title", content: page_title)
- meta(name: "twitter:description", content: site.description)
-
- link(rel: "alternate", type: "application/rss+xml", title: "RSS Feed", href: site.url_for('/feed.xml'))
- link(rel: "alternate", type: "application/json", title: "JSON Feed", href: site.url_for('/feed.json'))
+ 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://use.typekit.net')
+ link(rel: 'dns-prefetch', href: 'https://netdna.bootstrapcdn.com')
+ link(rel: 'dns-prefetch', href: 'https://gist.github.com')
all_styles.each do |style|
- link(rel: "stylesheet", href: site.url_for("/#{style.href}"))
+ link(rel: 'stylesheet', type: 'text/css', href: absolute_asset(style.href))
end
end
body do
render_header
- main(&block)
+ instance_exec(&block) if block
render_footer
render_scripts
end
@@ -57,6 +102,20 @@ module Pressa
private
+ def description
+ page_description || site.description
+ end
+
+ def full_title
+ return site.title unless page_subtitle
+
+ "#{site.title}: #{page_subtitle}"
+ end
+
+ def og_image_url
+ site.image_url
+ end
+
def all_styles
site.styles + page_styles
end
@@ -66,37 +125,77 @@ module Pressa
end
def render_header
- header do
- h1 { a(href: "/") { site.title } }
- nav do
- ul do
- li { a(href: "/") { "Home" } }
- li { a(href: "/posts/") { "Archive" } }
- li { a(href: "/projects/") { "Projects" } }
- li { a(href: "/about/") { "About" } }
+ 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 }
end
end
+
+ nav(class: 'remote') do
+ ul do
+ li(class: 'mastodon') do
+ a(rel: 'me', href: 'https://techhub.social/@sjs') do
+ i(class: 'fab fa-mastodon')
+ end
+ end
+ li(class: 'github') do
+ a(href: 'https://github.com/samsonjs') do
+ i(class: 'fab fa-github')
+ end
+ end
+ li(class: 'rss') do
+ a(href: site.url_for('/feed.xml')) do
+ i(class: 'fa fa-rss')
+ end
+ end
+ end
+ end
+
+ 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' } }
+ end
+ end
+
+ div(class: 'clearfix')
end
end
def render_footer
- footer do
- p { "© #{Time.now.year} #{site.author}" }
- p do
- plain "Email: "
- a(href: "mailto:#{site.email}") { site.email }
- end
+ footer(class: 'container') do
+ plain "© #{START_YEAR} - #{Time.now.year} "
+ a(href: site.url_for('/about')) { site.author }
end
end
def render_scripts
- all_scripts.each do |script|
- if script.defer
- script_(src: site.url_for("/#{script.src}"), defer: true)
- else
- script_(src: site.url_for("/#{script.src}"))
- end
+ all_scripts.each do |scr|
+ attrs = { src: script_src(scr.src) }
+ attrs[:defer] = true if scr.defer
+ script(**attrs)
end
+
+ script(src: 'https://use.typekit.net/tcm1whv.js', crossorigin: 'anonymous')
+ script { plain 'try{Typekit.load({ async: true });}catch(e){}' }
+ end
+
+ def script_src(src)
+ return src if src.start_with?('http://', 'https://')
+
+ absolute_asset(src)
+ end
+
+ def absolute_asset(path)
+ normalized = path.start_with?('/') ? path : "/#{path}"
+ site.url_for(normalized)
end
end
end
diff --git a/pressa/lib/views/month_posts_view.rb b/pressa/lib/views/month_posts_view.rb
index 621f7b5..b07f7a7 100644
--- a/pressa/lib/views/month_posts_view.rb
+++ b/pressa/lib/views/month_posts_view.rb
@@ -11,10 +11,12 @@ module Pressa
end
def view_template
- article(class: "month-posts") do
+ div(class: 'container') do
h1 { "#{@month_posts.month.name} #{@year}" }
+ end
- @month_posts.sorted_posts.each do |post|
+ @month_posts.sorted_posts.each do |post|
+ div(class: 'container') do
render PostView.new(post:, site: @site)
end
end
diff --git a/pressa/lib/views/post_view.rb b/pressa/lib/views/post_view.rb
index 45b36aa..120d23f 100644
--- a/pressa/lib/views/post_view.rb
+++ b/pressa/lib/views/post_view.rb
@@ -7,38 +7,43 @@ end
module Pressa
module Views
class PostView < Phlex::HTML
- def initialize(post:, site:)
+ def initialize(post:, site:, article_class: nil)
@post = post
@site = site
+ @article_class = article_class
end
def view_template
- article(class: "post") do
+ article(**article_attributes) do
header do
- if @post.link_post?
- h1 do
+ h2 do
+ if @post.link_post?
a(href: @post.link) { "→ #{@post.title}" }
+ else
+ a(href: @post.path) { @post.title }
end
- else
- h1 { @post.title }
- end
-
- div(class: "post-meta") do
- time(datetime: @post.date.iso8601) { @post.formatted_date }
- plain " · "
- a(href: @site.url_for(@post.path), class: "permalink") { "Permalink" }
end
+ time { @post.formatted_date }
+ a(href: @post.path, class: 'permalink') { '∞' }
end
- div(class: "post-content") do
- raw(@post.body)
- end
+ raw(@post.body)
+ end
- footer(class: "post-footer") do
- div(class: "fin") { "◼" }
+ div(class: 'row clearfix') do
+ p(class: 'fin') do
+ i(class: 'fa fa-code')
end
end
end
+
+ private
+
+ def article_attributes
+ return {} unless @article_class
+
+ { class: @article_class }
+ end
end
end
end
diff --git a/pressa/lib/views/project_view.rb b/pressa/lib/views/project_view.rb
index 2a45684..30f9929 100644
--- a/pressa/lib/views/project_view.rb
+++ b/pressa/lib/views/project_view.rb
@@ -9,36 +9,54 @@ module Pressa
end
def view_template
- article(class: "project", data_title: @project.github_path) do
- header do
- h1 { @project.title }
- p(class: "description") { @project.description }
+ article(class: 'container project') do
+ h1(id: 'project', data: { title: @project.title }) { @project.title }
+ h4 { @project.description }
+
+ div(class: 'project-stats') do
p do
- a(href: @project.url) { "View on GitHub →" }
+ 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')
end
end
- section(class: "project-stats") do
- h2 { "Statistics" }
- div(id: "stats") do
- p { "Loading..." }
+ div(class: 'project-info row clearfix') do
+ div(class: 'column half') do
+ h3 { 'Contributors' }
+ div(id: 'contributors')
end
- end
- section(class: "project-contributors") do
- h2 { "Contributors" }
- div(id: "contributors") do
- p { "Loading..." }
- end
- end
-
- section(class: "project-languages") do
- h2 { "Languages" }
- div(id: "languages") do
- p { "Loading..." }
+ div(class: 'column half') do
+ h3 { 'Languages' }
+ div(id: 'langs')
end
end
end
+
+ div(class: 'row clearfix') do
+ p(class: 'fin') do
+ i(class: 'fa fa-code')
+ end
+ end
+
+ end
+
+ private
+
+ def stargazers_url
+ "#{@project.url}/stargazers"
+ end
+
+ def network_url
+ "#{@project.url}/network/members"
end
end
end
diff --git a/pressa/lib/views/projects_view.rb b/pressa/lib/views/projects_view.rb
index 19c216c..791ad19 100644
--- a/pressa/lib/views/projects_view.rb
+++ b/pressa/lib/views/projects_view.rb
@@ -9,20 +9,24 @@ module Pressa
end
def view_template
- article(class: "projects") do
- h1 { "Projects" }
+ article(class: 'container') do
+ h1 { 'Projects' }
- p { "Open source projects I've created or contributed to." }
-
- ul(class: "projects-list") do
- @projects.each do |project|
- li do
+ @projects.each do |project|
+ div(class: 'project-listing') do
+ h4 do
a(href: @site.url_for(project.path)) { project.title }
- plain " – #{project.description}"
end
+ p(class: 'description') { project.description }
end
end
end
+
+ div(class: 'row clearfix') do
+ p(class: 'fin') do
+ i(class: 'fa fa-code')
+ end
+ end
end
end
end
diff --git a/pressa/lib/views/recent_posts_view.rb b/pressa/lib/views/recent_posts_view.rb
index 4e59886..09d6586 100644
--- a/pressa/lib/views/recent_posts_view.rb
+++ b/pressa/lib/views/recent_posts_view.rb
@@ -10,7 +10,7 @@ module Pressa
end
def view_template
- div(class: "recent-posts") do
+ div(class: 'container') do
@posts.each do |post|
render PostView.new(post:, site: @site)
end
diff --git a/pressa/lib/views/year_posts_view.rb b/pressa/lib/views/year_posts_view.rb
index 88a7c3a..1a9a543 100644
--- a/pressa/lib/views/year_posts_view.rb
+++ b/pressa/lib/views/year_posts_view.rb
@@ -10,8 +10,10 @@ module Pressa
end
def view_template
- article(class: "year-posts") do
- h1 { @year.to_s }
+ div(class: 'container') do
+ h2(class: 'year') do
+ a(href: year_path) { @year.to_s }
+ end
@year_posts.sorted_months.each do |month_posts|
render_month(month_posts)
@@ -21,30 +23,36 @@ module Pressa
private
+ def year_path
+ @site.url_for("/posts/#{@year}/")
+ end
+
def render_month(month_posts)
month = month_posts.month
- section(class: "month") do
- h2 do
- a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do
- month.name
- end
+ h3(class: 'month') do
+ a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do
+ month.name
end
+ end
- ul do
- month_posts.sorted_posts.each do |post|
- li do
- if post.link_post?
- a(href: post.link) { "→ #{post.title}" }
- else
- a(href: @site.url_for(post.path)) { post.title }
- end
- plain " – #{post.formatted_date}"
- end
+ ul(class: 'archive') do
+ month_posts.sorted_posts.each do |post|
+ li do
+ a(href: post_link(post)) { post.title }
+ time { short_date(post.date) }
end
end
end
end
+
+ def post_link(post)
+ post.link_post? ? post.link : @site.url_for(post.path)
+ end
+
+ def short_date(date)
+ date.strftime('%-d %b')
+ end
end
end
end
diff --git a/pressa/spec/examples.txt b/pressa/spec/examples.txt
index 0e06a65..a9f0bc0 100644
--- a/pressa/spec/examples.txt
+++ b/pressa/spec/examples.txt
@@ -1,17 +1,20 @@
example_id | status | run_time |
------------------------------------------------- | ------ | --------------- |
-./spec/posts/metadata_spec.rb[1:1:1] | passed | 0.00009 seconds |
-./spec/posts/metadata_spec.rb[1:1:2] | passed | 0.00032 seconds |
-./spec/posts/metadata_spec.rb[1:1:3] | passed | 0.00228 seconds |
-./spec/posts/repo_spec.rb[1:1:1] | passed | 0.00078 seconds |
-./spec/posts/repo_spec.rb[1:1:2] | passed | 0.01536 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:1] | passed | 0.00024 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:2] | passed | 0.00003 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:3] | passed | 0.00006 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:4] | passed | 0.00004 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:5] | passed | 0.00004 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:6] | passed | 0.00003 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:7] | passed | 0.00042 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:1:8] | passed | 0.00013 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:2:1] | passed | 0.00059 seconds |
-./spec/utils/frontmatter_converter_spec.rb[1:2:2] | passed | 0.00004 seconds |
+./spec/posts/metadata_spec.rb[1:1:1] | passed | 0.00025 seconds |
+./spec/posts/metadata_spec.rb[1:1:2] | passed | 0.00089 seconds |
+./spec/posts/metadata_spec.rb[1:1:3] | passed | 0.00068 seconds |
+./spec/posts/repo_spec.rb[1:1:1] | passed | 0.0172 seconds |
+./spec/posts/repo_spec.rb[1:1:2] | passed | 0.0009 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:1] | passed | 0.0002 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:2] | passed | 0.00004 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:3] | passed | 0.00003 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:4] | passed | 0.00056 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:5] | passed | 0.00003 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:6] | passed | 0.00032 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:7] | passed | 0.00003 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:1:8] | passed | 0.00004 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:2:1] | passed | 0.00004 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:2:2] | passed | 0.00003 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:2:3] | passed | 0.00002 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:2:4] | passed | 0.00003 seconds |
+./spec/utils/frontmatter_converter_spec.rb[1:2:5] | passed | 0.00002 seconds |
diff --git a/pressa/spec/utils/frontmatter_converter_spec.rb b/pressa/spec/utils/frontmatter_converter_spec.rb
index 7f6ab42..a33211a 100644
--- a/pressa/spec/utils/frontmatter_converter_spec.rb
+++ b/pressa/spec/utils/frontmatter_converter_spec.rb
@@ -18,10 +18,10 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
output = described_class.convert_content(input)
expect(output).to start_with("---\n")
- expect(output).to include("Title: Test Post")
+ expect(output).to include("Title: \"Test Post\"")
expect(output).to include("Author: Sami Samhuri")
expect(output).to include("Date: \"11th November, 2025\"")
- expect(output).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
+ expect(output).to include("Timestamp: 2025-11-11T14:00:00-08:00")
expect(output).to end_with("---\n\nThis is the post body.\n")
end
@@ -45,8 +45,8 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
output = described_class.convert_content(input)
- expect(output).to include("Title: Zelda Tones for iOS")
- expect(output).to include("Tags: \"zelda, nintendo, pacman, ringtones, tones, ios\"")
+ expect(output).to include("Title: \"Zelda Tones for iOS\"")
+ expect(output).to include("Tags:\n - zelda\n - nintendo\n - pacman\n - ringtones\n - tones\n - ios")
expect(output).to include("
Zelda
")
end
@@ -67,7 +67,7 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
output = described_class.convert_content(input)
expect(output).to include("Link: http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo")
- expect(output).to include("Tags: \"amusement, buffalo\"")
+ expect(output).to include("Tags:\n - amusement\n - buffalo")
end
it 'converts front-matter with Scripts and Styles' do
@@ -105,7 +105,7 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
output = described_class.convert_content(input)
expect(output).to include("Date: \"1st January, 2025\"")
- expect(output).to include("Timestamp: \"2025-01-01T12:00:00-08:00\"")
+ expect(output).to include("Timestamp: 2025-01-01T12:00:00-08:00")
end
it 'raises error if no front-matter delimiter' do
@@ -165,11 +165,11 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
yaml = described_class.convert_frontmatter_to_yaml(input)
- expect(yaml).to include("Title: Test Post")
+ expect(yaml).to include("Title: \"Test Post\"")
expect(yaml).to include("Author: Sami Samhuri")
expect(yaml).to include("Date: \"11th November, 2025\"")
- expect(yaml).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
- expect(yaml).to include("Tags: \"Ruby, Testing\"")
+ expect(yaml).to include("Timestamp: 2025-11-11T14:00:00-08:00")
+ expect(yaml).to include("Tags:\n - Ruby\n - Testing")
expect(yaml).to include("Link: https://example.net")
expect(yaml).to include("Scripts: app.js")
expect(yaml).to include("Styles: style.css")
@@ -187,8 +187,48 @@ RSpec.describe Pressa::Utils::FrontmatterConverter do
yaml = described_class.convert_frontmatter_to_yaml(input)
- expect(yaml).to include("Title: Test")
+ expect(yaml).to include("Title: \"Test\"")
expect(yaml).to include("Author: Sami Samhuri")
end
+
+ it 'handles multi-word field names' do
+ input = <<~FRONTMATTER
+ Title: About Page
+ Page type: page
+ Show extension: false
+ Date: 1st January, 2025
+ Timestamp: 2025-01-01T12:00:00-08:00
+ FRONTMATTER
+
+ yaml = described_class.convert_frontmatter_to_yaml(input)
+
+ expect(yaml).to include("Page type: page")
+ expect(yaml).to include("Show extension: false")
+ end
+
+ it 'escapes embedded quotes in values' do
+ input = <<~FRONTMATTER
+ Title: The "Swift" Programming Language
+ Date: 1st January, 2025
+ Timestamp: 2025-01-01T12:00:00-08:00
+ FRONTMATTER
+
+ yaml = described_class.convert_frontmatter_to_yaml(input)
+
+ expect(yaml).to include('Title: "The \"Swift\" Programming Language"')
+ expect(yaml).not_to include('Title: "The "Swift" Programming Language"')
+ end
+
+ it 'escapes backslashes in values' do
+ input = <<~FRONTMATTER
+ Title: Paths\\and\\backslashes, oh my
+ Date: 1st January, 2025
+ Timestamp: 2025-01-01T12:00:00-08:00
+ FRONTMATTER
+
+ yaml = described_class.convert_frontmatter_to_yaml(input)
+
+ expect(yaml).to include('Title: "Paths\\\\and\\\\backslashes, oh my"')
+ end
end
end