mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
WIP: close to original output
This commit is contained in:
parent
6e9988fd29
commit
cf3dc9afb6
28 changed files with 893 additions and 314 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
66
pressa/bin/convert-all-posts
Executable file
66
pressa/bin/convert-all-posts
Executable file
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
91
pressa/lib/utils/html_formatter.rb
Normal file
91
pressa/lib/utils/html_formatter.rb
Normal file
|
|
@ -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 class="typocode">.*?<\/div>/m,
|
||||
/<div class="pressa">.*?<\/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?('<!')
|
||||
end
|
||||
|
||||
def self.closing_tag?(line)
|
||||
line.start_with?('</')
|
||||
end
|
||||
|
||||
def self.self_closing?(line)
|
||||
line.end_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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
pressa/lib/views/feed_post_view.rb
Normal file
24
pressa/lib/views/feed_post_view.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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("<h2>Zelda</h2>")
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue