Showing posts with label Rake. Show all posts
Showing posts with label Rake. Show all posts

Tuesday, 31 March 2009

Using Haml & Sass from a Rake task

Haml logoSome time ago I had the 'lightning' idea to implement another Rake automation to support my current blogging workflow, which at the moment consists of finding a sparkling idea to blog about, write it out in WriteRoom and refine the post in TextMate before publishing. As this process was a recurring and copy & paste driven event, I strove for an automation supporting this workflow. So unsurprisingly the post will show my current solution to achieve this goal by utilizing Rake, Haml and Sass.

So what's that Haml and Sass thingy?

Haml (HTML Abstraction Markup Language) is a templating language/engine with the primary goal to make Markup DRY, beautiful and readable again. It has a very shallow learning curve and therefor is perfectly suited for programmers and designers alike. Haml is primarily targeted at making the views of Ruby on Rails, Merb or Sinatra web applications leaner, but as you will see later the Ruby implementation also can be used framework independently.

Sass (Syntactically Awesome StyleSheets) is a module which comes bundled with Haml providing a meta-language/abstraction on top of CSS sharing the same goals and advantages as Haml.

Gluing Haml and Sass into a Rake task

To get going you first have to install Haml and Sass by running the gem command shown next.
sudo gem install haml
With Haml and Sass available it's about time to identify and outline the parts you want to automate, in my case it's the creation of a WriteRoom and/or a XHTML draft document for initial editings. So the parameters to pass into the task to come are the targeted editor(s), the title of the blog post to draft and a list of associated and whitespace separated category tags.

The XHTML document skeleton content and it's inline CSS are defined each in a separate Haml and Sass template file and will be rendered into the outcoming document along with the content passed into the Rake task. While the document skeleton for the WriteRoom draft document, due to it's brevity, is defined inside of the task itself. The following snippets are showing the mentioned Haml and Sass templates for the XHTML draft output file, which are located in the same directory as the Rake file.

 Haml
!!! 1.1
%html
%head
%title= "#{title} - Draft"
%style{ :type => 'text/css' }= inline_css
%body
%h3= title
%h4.custom sub headline
%pre.consoleOutput console command
%pre.codeSnippet code snippet
%br/
= "Tags: #{tags.join ', '}"
 Sass
body
:margin 5
:line-height 1.5em
:font small Trebuchet MS, Verdana, Arial, Sans-serif
:color #000000
h4
:margin-bottom 0.3em
.consoleOutput
:padding 6px
:background-color #000
:color rgb(20, 218, 62)
:font-size 12px
:font-weight bolder
.codeSnippet
:padding 3px
:background-color rgb(243, 243, 243)
:color rgb(93, 91, 91)
:font-size small
:border 1px solid #6A6565
To inject the dynamic content into the Haml template and have it rendered into the outcoming document, the values i.e. draft_title, draft_tags and draft_inline_css have to be made available to the template engine by passing them in a bundling Hash into the to_html alias method of the Haml Engine object like shown in the next Rake task.
task :default do
Rake::Task['blog_utils:create_draft_doc'].invoke
end

namespace :blog_utils do

desc 'Create a new draft document for a given title, category tags and editor'
task :create_draft_doc, [:title, :tags, :editor] do |t, args|
draft_title = args.title
draft_tags = args.tags.split(' ')
draft_target_editor = args.editor

raise_message = 'No title for draft provided'
raise raise_message if draft_title.nil?

raise_message = 'No tags for draft provided'
raise raise_message if draft_tags.nil?

draft_target_editor = '*' if draft_target_editor.nil?

raise_message = 'Unsupported target editor provided'
raise raise_message unless draft_target_editor == 'Textmate' ||
draft_target_editor == 'Writeroom' || draft_target_editor == '*'

if draft_target_editor == 'Writeroom' || draft_target_editor == '*'
draft_output_file = draft_title.gsub(' ', '_') + '.txt'

File.open(draft_output_file, 'w') do |draft_file_txt|
draft_file_txt.puts draft_title
draft_file_txt.puts
draft_file_txt.puts "Tags: #{draft_tags.join ', '}"
end
end

if draft_target_editor == 'Textmate' || draft_target_editor == '*'

template_sass_content, template_haml_content = ''

['haml', 'sass'].each do |template_type|
template = File.dirname(__FILE__) + "/draft_template.#{template_type}"
raise_message = "#{template_type.capitalize} template '#{template}' not found"
raise raise_message if !File.exists?(template)

template_sass_content = File.read(template) if template_type === 'sass'
template_haml_content = File.read(template) if template_type === 'haml'
end

require 'sass'
require 'haml'

draft_inline_css = Sass::Engine.new(template_sass_content).to_css
draft_document_content = Haml::Engine.new(template_haml_content).to_html(
Object.new, { :title => draft_title , :tags => draft_tags ,
:inline_css => draft_inline_css } )


draft_output_file = draft_title.gsub(' ', '_') + '.html'
File.open(draft_output_file, 'w') do |draft_file_html|
draft_file_html.puts(draft_document_content)
end
end

end
end

Easing invocation pain with alias

Now as the Rake task is implemented and waiting for demands it can be invoked by calling the task as shown in the next console snippet.
sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc['Title','Tag1 TagN','Editor']
As I'm not even close to being a console ninja and probably will have forgotten the task call structure before initiating the next blog post, I decided to add an easing and more memorizable alias to $HOME/.profile as shown next.
alias createdraft='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc[$title,$tags,$editor]'
The created alias now allows to invoke the Rake task in a nice and easy way as shown in the next console command.
createdraft title='Using Haml & Sass from a Rake task' tags='Rake Ruby' editor='Textmate'

Taking a peek at the generated draft document

After running the described Rake task I end up with the XHTML document shown in the outro code snippet, which then can be used for the further editing process. Of course I could have setup a TextMate Snippet to get me going, but that way I would have missed the opportunity to mess around with another amazing Ruby tool.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
<head>
<title>Using Haml & Sass from a Rake task - Draft</title>
<style type='text/css'>
body {
margin: 5;
line-height: 1.5em;
font: small Trebuchet MS, Verdana, Arial, Sans-serif;
color: #000000; }

h4 {
margin-bottom: 0.3em; }

.consoleOutput {
padding: 6px;
background-color: #000;
color: rgb(20, 218, 62);
font-size: 12px;
font-weight: bolder; }

.codeSnippet {
padding: 3px;
background-color: rgb(243, 243, 243);
color: rgb(93, 91, 91);
font-size: small;
border: 1px solid #6A6565; }

</style>
</head>
<body>
<h3>Using Haml & Sass from a Rake task</h3>
<h4>sub headline</h4>
<pre class='consoleOutput'>console command</pre>
<pre class='codeSnippet'>code snippet</pre>
<br />
Tags: Rake, Ruby
</body>
</html>

Saturday, 24 January 2009

Broadcasting blog post notifications to Twitter with Ruby and Rake

Blogger to Twitter LogoDuring my latest blogging absence I had some time to tinker around with Ruby. For an introductory challenge I chose to implement a real life feature which currently isn't supported by Blogger.com and screams siren-like for an one-button automation: Broadcasting the latest blog entry to my Twitter account. As I didn't want to sign up for a Twitterfeed account and couldn't resort to the Twitter Tools plugin like WordPress users, I had to perform these broadcasting steps manually, until now. To see how this repetitive and time-stealing process was transformed into a semi-automated one by utilizing Ruby, a splash of Hpricot, Ruby's excellent Twitter Api wrapper and Rake, read on my dear.

Installing the required RubyGems

Prior to diving into the implementation details of the given scenario I had to install the required RubyGems like shown in the next console snippet. The installation of the twitter gem might take a while due to it's dependency on several other gems.
sudo gem install hpricot rake twitter

Scraping the latest blog post details with Hpricot

The initial implementation step was to gather relevant metadata (Url, title and used tags) of the latest blog post. I first took the route to get it by grabbing the blog's RSS feed and extracting the metadata from there, but soon stumbled into problems getting an outdated feed from Feedburner. The next alternative was to scrape the needed metadata directly from the blog landing page. As I went this route before with the Zend_Dom_Query component of the Zend Framework I decided to use something similar from the Ruby toolbox. Some Google hops later I was sold to Hpricot, a HTML Parser for Ruby and as you can see in the first code snippet, showing an extract of the Rake file to come, this is done in just 13 lines of code.
doc = Hpricot(open(blog_landing_page, scrape_options))
latest_post_url = doc.at('h3.post-title > a')['href']
latest_post_title = doc.at('h3.post-title > a').inner_html
label_doc = Hpricot(doc.search('span.post-labels').first.to_s)
label_links = label_doc.search('span.post-labels > a').each do |label_link|
label = label_link.inner_html.gsub(' ', '').downcase
if label.include?('/')
labels = label.split('/')
labels.each { |label| last_post_labels.push(label) }
else
last_post_labels.push(label)
end
end

Outstanding tasks

With the metadata available the oustanding tasks to implement were:
  • to get a short Url for the actual blog post by utilzing a public API of an Url shortening service i.e. is.gd
  • to build the tweet to broadcast by injecting the available metadata into a tweet template
  • to broadcast the notification tweet to the given Twitter account
  • to log the broadcasted blog title to prevent spamming or duplication scenarios
As a guy sold to build tools and eager to learn something new I subverted Rake, Ruby's number one build language, to glue the above mentioned tasks and their implementation together, to manage their sequential dependencies and to have a comfortable invocation interface. The nice thing about Rake is that it allows you to implement each tasks unit of work by using the Ruby language; and there is no need to follow a given structure to implement custom tasks like it's the case for custom Phing tasks. As you will see in the forthcoming complete Rakefile some of the tasks are getting quite long and complex; therefor some of them are pending candidates for Refactoring activities like for example extract task units of work into helper/worker classes.
  require 'rubygems'
require 'hpricot'
require 'open-uri'
require 'twitter'

task :default do
Rake::Task['blog_utils:broadcast_notification'].invoke
end

namespace :blog_utils do

scrape_options = { 'UserAgent' => "Ruby/#{RUBY_VERSION}" }
blog_landing_page = 'http://raphaelstolt.blogspot.com'
latest_post_short_url, latest_post_url, latest_post_title = nil
notification_tweet = nil
last_post_labels = []
broadcast_log_file = File.dirname(__FILE__) + '/broadcasted_posts.log'
twitter_credentials = { :user => 'raphaelstolt', :pwd => 'thatsasecret'}

desc 'Scrape metadata of latest blog post from landing page'
task :scrape_actual_post_metadata do
doc = Hpricot(open(blog_landing_page, scrape_options))
latest_post_url = doc.at('h3.post-title > a')['href']
latest_post_title = doc.at('h3.post-title > a').inner_html
label_doc = Hpricot(doc.search('span.post-labels').first.to_s)
label_links = label_doc.search('span.post-labels > a').each do |label_link|
label = label_link.inner_html.gsub(' ', '').downcase
if label.include?('/')
labels = label.split('/')
labels.each { |label| last_post_labels.push(label) }
else
last_post_labels.push(label)
end
end
end

desc 'Shorten the Url of the latest blog post'
task :shorten_post_url => [:scrape_actual_post_metadata] do
raise_message = 'No Url for latest blog post available'
raise raise_message if latest_post_url.nil?
url_shorten_service_call = "http://is.gd/api.php?longurl=#{latest_post_url}"
latest_post_short_url = open(url_shorten_service_call, scrape_options).read
end

desc 'Check if generate shorten Url references the latest blog post url'
task :check_shorten_url_references_latest do
url_referenced_by_short_url = nil
open(latest_post_short_url, scrape_options) do |f|
url_referenced_by_short_url = f.base_uri.to_s
end
raise_message = "Generated short Url '#{latest_post_short_url}' does not"
raise_message << " reference actual blog post url '#{latest_post_url}'"
raise raise_message unless url_referenced_by_short_url.eql?(latest_post_url)
end

desc 'Check if latest blog post has already been broadcasted'
task :check_logged_broadcasts do
logged_broadcasts = []
if File.exist?(broadcast_log_file)
File.open(broadcast_log_file, 'r') do |f|
logged_broadcasts = f.readlines.collect { |line| line.chomp }
end
end
raise_message = "Blog post '#{latest_post_title}' has already been "
raise_message << "broadcasted"
raise raise_message if logged_broadcasts.include?(latest_post_title)
end

desc 'Build notification tweet by injecting scraped metadata into template'
task :build_notification_tweet => [:shorten_post_url,
:check_shorten_url_references_latest] do
raise_message = 'Required metadata to build tweet is not available'
raise raise_message if latest_post_title.nil? || latest_post_short_url.nil?
raise raise_message if last_post_labels.nil?

notification_tweet = "Published a new blog post '#{latest_post_title}' "
notification_tweet << "available at #{latest_post_short_url}."

raise_message = 'Broadcast for latest blog post exceeds 140 characters'
raise raise_message if notification_tweet.length > 140

last_post_labels.each do |tag|
notification_tweet << " ##{tag}" unless notification_tweet.length +
" ##{tag}".length > 140
end
end

desc 'Broadcast latest blog post notification to twitter'
task :broadcast_notification_to_twitter => [:build_notification_tweet,
:check_logged_broadcasts] do
raise_message = "Notification tweet to broadcast is not available"
raise raise_message if notification_tweet.nil?
puts "Broadcasting '#{notification_tweet}'"
http_auth = Twitter::HTTPAuth.new(twitter_credentials[:user], twitter_credentials[:pwd])
Twitter::Base.new(http_auth).update(notification_tweet)
#Twitter::Base.new(twitter_credentials[:user], twitter_credentials[:pwd]).post(notification_tweet)
Rake::Task['blog_utils:log_broadcast_title'].invoke
end

desc 'Log broadcasted blog post title'
task :log_broadcast_title do
puts "Logging latest post title to #{broadcast_log_file}"
File.open(broadcast_log_file, 'a') do |f|
f.puts latest_post_title
end
end

end

Putting the Rake task(s) to work

The next step was to put the Rakefile into my $HOME directory; and after publishing a new blog post I'm now able to broadcast an automated notification by firing up the console and calling the Rake task like shown next.
sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification
And as I'm too lazy to type this lengthy command everytime I further added an alias to the $HOME/.profile file which allows me to call the task via the associated alias i.e. blogger2twitter shown in the .profile excerpt.
alias blogger2twitter='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification'
After running the Rake task against this blog post the notification gets added to the given Twitter timeline like shown in the outro image.

Notification tweet screenshot