#!/usr/bin/env ruby -w # encoding: utf-8 require 'rubygems' require 'time' require 'bundler/setup' require 'builder' require 'json' require 'rdiscount' require 'mustache' class Hash def compact h = {} each_pair do |k, v| h[k] = v unless v.nil? end h end end def main dir = ARGV.shift.to_s unless File.directory? dir puts 'usage: feeds ' exit 1 end b = Blag.new dir b.generate! end class Blag def self.go! dir self.new(dir).generate! end def initialize dir, index_posts = 10, feed_posts = 30 @dir = dir @index_posts = index_posts @feed_posts = feed_posts end def generate! generate_latest_posts_json generate_rss generate_json_feed end def generate_latest_posts_json data['posts'] = latest_posts(@index_posts).map do |post| { title: post['title'], date: post['date'], timestamp: post['timestamp'], tags: post['tags'], author: post['author'], url: post['relative_url'], link: post['link'], styles: post['styles'] }.delete_if { |k, v| v.nil? } end json = JSON.pretty_generate data File.write(data_file, json) end def generate_rss File.write(rss_file, feed_xml.target!) end def generate_json_feed File.write(json_feed_file, feed_json) end def latest_posts(n) posts.first(n) end def find_post dir, slug # technically should look for slug.md, slug.html.md, etc. File.join dir, slug + '.md' end def posts @posts ||= begin Dir[File.join(@dir, 'posts/20*/*')].map do |dir| next unless dir =~ /\/\d\d$/ json = File.read File.join(dir, '_data.json') data = JSON.parse json prefix = dir.sub(@dir, '') data.map do |slug, post| filename = find_post dir, slug content = File.read filename relative_url = File.join(prefix, slug) post['slug'] = slug post['type'] = post['link'] ? :link : :post post['title'] = "→ #{post['title']}" if post['type'] == :link post['relative_url'] = relative_url post['url'] = root_url + post['relative_url'] post['external_url'] post['content'] = content post['body'] = RDiscount.new(content, :smart).to_html post['time'] = Time.at(post['timestamp']) post end end.flatten.compact.sort_by { |p| -p['timestamp'] } end end def title data['title'] end def subtitle data['subtitle'] end def root_url data['url'] end private def data_file File.join @dir, '_data.json' end def data @data ||= JSON.parse File.read(data_file) end def feed_template(type) if type == :post @post_rss_template ||= File.read(File.join('templates', 'post.feed.html')) elsif type == :link @link_rss_template ||= File.read(File.join('templates', 'link.feed.html')) else raise 'unknown post type: ' + type end end def rss_file File.join @dir, 'feed.xml' end def feed_html post html = Mustache.render feed_template(post['type']), post: post # this is pretty disgusting html.gsub 'src="/', "src=\"#{root_url}/" end def feed_xml xml = Builder::XmlMarkup.new xml.instruct! :xml, version: '1.0' xml.instruct! 'xml-stylesheet', href: root_url + '/css/normalize.css', type: 'text/css' xml.instruct! 'xml-stylesheet', href: root_url + '/css/style.css', type: 'text/css' xml.rss version: '2.0' do xml.channel do xml.title title xml.description subtitle xml.link root_url xml.pubDate posts.first['time'].rfc822 latest_posts(@feed_posts).each do |post| xml.item do xml.title post['title'] xml.description feed_html(post) xml.pubDate post['time'].rfc822 xml.author post['author'] xml.link post['link'] || post['url'] xml.guid post['url'] end end end end xml end def json_feed_file File.join @dir, 'feed.json' end def feed_json JSON.pretty_generate(build_json_feed) end def build_json_feed { version: "https://jsonfeed.org/version/1", title: title, home_page_url: root_url, feed_url: "#{root_url}/feed.json", author: { url: "https://samhuri.net", name: "Sami Samhuri", avatar: "#{root_url}/images/me.jpg" }, icon: "#{root_url}/images/apple-touch-icon-300.png", favicon: "#{root_url}/images/apple-touch-icon-80.png", items: latest_posts(@feed_posts).map do |post| { title: post['title'], date_published: post['time'].to_datetime.rfc3339, id: post['url'], url: post['url'], external_url: post['link'], author: { name: post['author'] }, content_html: feed_html(post), tags: post['tags'] }.compact end } end end main if $0 == __FILE__