├── .gitignore ├── views ├── home.haml ├── article.haml ├── _article.haml ├── feed.haml ├── layout.haml └── stylesheets │ └── screen.sass ├── public ├── favicon.ico ├── images │ ├── booty.jpg │ └── ruby_art_and_all_things.png ├── assets │ └── jquery_toggler.html └── javascripts │ └── code_highlighter.js ├── Gemfile ├── Gemfile.lock ├── Readme.textile ├── articles ├── load_gems_in_test_rb.haml ├── railscamp_six.haml ├── active_record_association_methods.haml ├── gem_bundler.haml ├── homebrew_ruby_odbc.haml ├── thinking_sphinx_dreamhost.haml ├── enter_sinatra.haml ├── translate_legacy_attributes.haml ├── sinatra_on_dreamhost.haml ├── jquery_toggler.haml ├── tasteful_routes.haml └── date_range_string_conversions.haml ├── app.rb └── lib └── article.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | deploy.yml 3 | config.ru 4 | tmp 5 | -------------------------------------------------------------------------------- /views/home.haml: -------------------------------------------------------------------------------- 1 | - for @article in @articles 2 | = haml(:_article, :layout => false) -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hughevans/hughevans.net/master/public/favicon.ico -------------------------------------------------------------------------------- /public/images/booty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hughevans/hughevans.net/master/public/images/booty.jpg -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'sinatra', '1.2.3' 4 | gem 'haml', '3.0.25' 5 | gem 'RedCloth', '4.2.7' 6 | -------------------------------------------------------------------------------- /public/images/ruby_art_and_all_things.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hughevans/hughevans.net/master/public/images/ruby_art_and_all_things.png -------------------------------------------------------------------------------- /views/article.haml: -------------------------------------------------------------------------------- 1 | = haml(:_article, :layout => false) 2 | 3 | #disqus_thread 4 | 5 | %script{:type => 'text/javascript', :src => 'http://disqus.com/forums/hughevans/embed.js'} -------------------------------------------------------------------------------- /views/_article.haml: -------------------------------------------------------------------------------- 1 | .article 2 | %h1.title 3 | - if @single_view 4 | = @article.title 5 | - else 6 | %a{:href => article_path(@article)} 7 | = @article.title 8 | .published 9 | Published: 10 | = @article.published.strftime("%B %d, %Y") 11 | = article_body(@article) -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | RedCloth (4.2.7) 5 | haml (3.0.25) 6 | rack (1.2.2) 7 | sinatra (1.2.3) 8 | rack (~> 1.1) 9 | tilt (< 2.0, >= 1.2.2) 10 | tilt (1.2.2) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | RedCloth (= 4.2.7) 17 | haml (= 3.0.25) 18 | sinatra (= 1.2.3) 19 | -------------------------------------------------------------------------------- /Readme.textile: -------------------------------------------------------------------------------- 1 | h1. hughevans.net 2 | 3 | _A Tiny Sinatra Blog_ 4 | 5 | My take on a tiny Sinatra blog, with inspiration (and a fair chunk of code) coming from "toolmantim":http://github.com/toolmantim/toolmantim/tree/master. 6 | 7 | h2. License 8 | 9 | MIT license except for the files found in: 10 | 11 | * @/articles/@ 12 | * @/public/images/@ 13 | 14 | They are copyright Hugh Evans. -------------------------------------------------------------------------------- /articles/load_gems_in_test_rb.haml: -------------------------------------------------------------------------------- 1 | -# title: Load Gems in test.rb 2 | -# published: 2009-03-01 3 | 4 | :textile 5 | Just a quick tip. Don't load gems that you only require for testing purposes in your Rails environment.rb. Put them in your test.rb instead, like so: 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | config.gem 'thoughtbot-shoulda', :lib => 'shoulda', :source => 'http://gems.github.com' 11 | 12 | :textile 13 | This works fine and will give you the added advantage of shaving off a few fractions from your production startup time. 14 | 15 | :textile 16 | Startup time is something of which I have become very sensitive to lately due to way Dreamhost kills off idle Passenger processes rather quickly. So if you too are hosting with Passenger pay careful attention to the startup time of your application. -------------------------------------------------------------------------------- /articles/railscamp_six.haml: -------------------------------------------------------------------------------- 1 | -# title: Railscamp VI 2 | -# published: 2009-11-23 3 | 4 | :textile 5 | There is much to be said about the latest "railscamp":http://railscamps.com/ instalment. Simply, it rocked. From "international speakers":http://topfunky.com/ to "multiplayer zombie rampages":http://github.com/chrislloyd/brains/, railscamp hasn't lost any of it's mojo. And that's despite it doubling in size. So big props must go out to "Ben Schwarz":http://germanforblack.com/, "John Barton":http://whoisjohnbarton.com and "Pat Allen":http://freelancing-gods.com for bringing it all together. 6 | 7 | Then there was the hacking. "Ben Webster":http://plus2.com.au, "Matt Allen":http://allen.com.au and myself made an app which will be used to benefit charities, swinging ourselves "some booty":/images/booty.jpg. I'll post some more info when we are closer to pushing it out. It's rather exciting really. 8 | 9 | -------------------------------------------------------------------------------- /views/feed.haml: -------------------------------------------------------------------------------- 1 | !!! xml 2 | %feed(xmlns='http://www.w3.org/2005/Atom') 3 | 4 | %title @hughevans - Ruby, Art and All things... 5 | %link(href='http://hughevans.net' rel='self' hreflang='en' type='application/atom+xml') 6 | 7 | %author 8 | %name Hugh Evans 9 | %uri http://hughevans.net/ 10 | 11 | %updated= @articles.first.published.xmlschema 12 | %id= "tag:hughevans.net,#{@articles.first.published.strftime('%Y-%m-%d')}:#{article_path(@articles.first)}" 13 | 14 | - for article in @articles 15 | %entry 16 | %id= "tag:hughevans.net,#{article.published.strftime('%Y-%m-%d')}:#{article_path(article)}" 17 | %title= article.title 18 | %published= article.published.xmlschema 19 | %updated= article.last_modified.xmlschema 20 | %link(href="http://hughevans.net#{article_path(article)}" rel='alternate' hreflang='en' type='text/html') 21 | %content(type='xhtml' xml:lang='en' xml:base='http://hughevans.net/') 22 | %div(xmlns='http://www.w3.org/1999/xhtml')= absoluteify_links(article_body(article)) -------------------------------------------------------------------------------- /articles/active_record_association_methods.haml: -------------------------------------------------------------------------------- 1 | -# title: Active Record Association Methods 2 | -# published: 2009-03-06 3 | 4 | :textile 5 | I often forget that you can define methods for an Active Record associations in a block after the association declaration like so: 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | class Order < ActiveRecord::Base 11 | has_many :items, :dependent => :destroy do 12 | def total 13 | inject(0) {|sum, s| sum += s.price} 14 | end 15 | end 16 | end 17 | 18 | %pre 19 | %code.ruby< 20 | :preserve 21 | >> @order = Order.last 22 | >> @order.items.total 23 | => 27.45 24 | 25 | :textile 26 | Most of the time this makes more sense than defining them as instance methods in the parent model and much more sense than class methods in the child because it will require the association to work anyhow (perhaps though I haven't chosen the best example to illustrate this as you could potentially use an Item.total method independently eg. Item.all.total, but you get the picture). 27 | 28 | 29 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require 'haml' 4 | require 'time' 5 | require './lib/article' 6 | 7 | Article.path = File.join(Sinatra::Application.root, 'articles') 8 | 9 | helpers do 10 | def article_body(article) 11 | haml(article.template, :layout => false) 12 | end 13 | 14 | def article_path(article) 15 | "/#{article.published.strftime("%Y/%m/%d")}/#{article.id}" 16 | end 17 | 18 | def absoluteify_links(html) 19 | html. 20 | gsub(/href=(["'])(\/.*?)(["'])/, 'href=\1http://hughevans.net\2\3'). 21 | gsub(/src=(["'])(\/.*?)(["'])/, 'src=\1http://hughevans.net\2\3') 22 | end 23 | end 24 | 25 | get '/' do 26 | @articles = Article.all.sort[0..10] 27 | haml :home 28 | end 29 | 30 | get '/:year/:month/:day/:id' do 31 | @article = Article[params[:id]] || raise(Sinatra::NotFound) 32 | @single_view = true 33 | haml :article 34 | end 35 | 36 | get '/articles.atom' do 37 | @articles = Article.all.sort 38 | content_type 'application/atom+xml' 39 | haml :feed, :layout => false 40 | end 41 | 42 | get '/:style.css' do 43 | content_type 'text/css', :charset => 'utf-8' 44 | sass :"stylesheets/#{params[:style]}" 45 | end 46 | 47 | module Haml::Filters::Preserve 48 | def render(text) 49 | Haml::Helpers.preserve(Haml::Helpers.html_escape(text)) 50 | end 51 | end -------------------------------------------------------------------------------- /articles/gem_bundler.haml: -------------------------------------------------------------------------------- 1 | -# title: Handling Deploys with Gem Bundler 2 | -# published: 2009-11-14 3 | 4 | :textile 5 | The best method of usage for "Gem Bundler":http://litanyagainstfear.com/blog/2009/10/14/gem-bundler-is-the-future/ we have found is to check in only your Gemfile and the gem cache dir, ignoring the rest in .gitignore: 6 | 7 | %pre 8 | %code< 9 | :preserve 10 | vendor/bundler_gems/environment.rb 11 | vendor/bundler_gems/gems 12 | vendor/bundler_gems/specifications 13 | 14 | :textile 15 | Now we have our known working .gem files checked in we can ensure that on deploy only these will be used on the server by passing Bundler the cached flag when deploying with Capistrano: 16 | 17 | %pre 18 | %code.ruby< 19 | :preserve 20 | after 'deploy:update_code', 'gems:bundle' 21 | 22 | namespace :gems do 23 | task :bundle, :roles => :app do 24 | run "cd \#{release_path} && gem bundle --cached" 25 | end 26 | end 27 | 28 | :textile 29 | Otherwise Bundler will look for updated gems and download them when you have not locked your Gemfile gems to specific versions. Stinging you big time, especially when APIs change on point releases.. 30 | 31 | :textile 32 | Anyhow, the reason you would want to do this as opposed to just checking in the whole vendor/bundler_gems dir is that you won't dirty up you history with thousand line commits when upgrading gems. 33 | -------------------------------------------------------------------------------- /articles/homebrew_ruby_odbc.haml: -------------------------------------------------------------------------------- 1 | -# title: Homebrew, FreeTDS and RubyODBC 2 | -# published: 2009-11-05 3 | 4 | :textile 5 | "Homebrew":http://github.com/mxcl/homebrew is awesome. On my new macbook I elected to use it in place of MacPorts and now I'm in love. I don't want to give you too much in the way of a sales pitch but all I'll say is that I feel I'm now that much closer to The Perfect Setup™ (Along with "RVM":http://rvm.beginrescueend.com, which is awesome as well). 6 | 7 | One thing I used Homebrew for was installing the packages required to get my Rails app talking to SQL Server. There already existed a formula for UnixODBC, but I had to write ones for FreeTDS and RubyODBC which was "simple":http://github.com/hughevans/homebrew/commit/acd8ccdb40f183d29e5e7c443d4d54fb04af4f3c "enough":http://github.com/hughevans/homebrew/commit/3387cd5a6c609d1ed24669adf4a0728c95befd5f (hopefully these will get pulled upstream soon). 8 | 9 | After this getting ODBC to work it was a simple matter of: 10 | 11 | %pre 12 | %code< 13 | :preserve 14 | brew install unixodbc 15 | brew install freetds 16 | brew install ruby-odbc 17 | 18 | Configure ~/.freetds.conf and ~/.odbc.ini 19 | 20 | :textile 21 | Combine this with the awesome "activerecord-sqlserver-adapter":http://github.com/rails-sqlserver/2000-2005-adapter/ and it all works a charm. I should also add that this is on Snow Leopard, hence the need for UnixODBC. -------------------------------------------------------------------------------- /views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! Strict 2 | %html(xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en') 3 | 4 | %head 5 | %meta(http-equiv='Content-Type' content='text/html; charset=utf-8') 6 | %title= "#{"#{@article.title} | " if @single_view }hughevans.net" 7 | %link(href='/screen.css' media='screen' rel='stylesheet' type='text/css') 8 | %link(rel='shortcut icon' href='/favicon.ico') 9 | %link(rel='alternate' href='/articles.atom' title='@hughevans - Ruby, Art and All things...' type='application/atom+xml') 10 | %link(href='http://fonts.googleapis.com/css?family=Copse|Lato|Droid+Sans+Mono' rel='stylesheet' type='text/css') 11 | %script(src='/javascripts/code_highlighter.js' type='text/javascript') 12 | %script(src='http://www.google-analytics.com/ga.js' type='text/javascript') 13 | 14 | %body 15 | #main 16 | .wrapper 17 | #header 18 | %h1 19 | %a(href='/') @hughevans 20 | = yield 21 | 22 | #footer 23 | .wrapper 24 | %strong 25 | my other stuff on the web 26 | %ul 27 | %li 28 | %a(href='http://twitter.com/hughevans') twitter 29 | %li 30 | %a(href='http://flickr.com/photos/hughevans/') flickr 31 | %li 32 | %a(href='http://github.com/hughevans') github 33 | 34 | :javascript 35 | try { 36 | var pageTracker = _gat._getTracker("UA-7674685-1"); 37 | pageTracker._trackPageview(); 38 | } catch(err) {}; 39 | -------------------------------------------------------------------------------- /articles/thinking_sphinx_dreamhost.haml: -------------------------------------------------------------------------------- 1 | -# title: Sphinx on Dreamhost 2 | -# published: 2009-03-10 3 | 4 | :textile 5 | If you wish to to install the "Sphinx":http://sphinxsearch.com/ search engine on Dreamhost, you can and it is really quite easy and painless. 6 | 7 | :textile 8 | The first step is to download, compile and install sphinx into your home directory (as you do not have permissions to install this elsewhere): 9 | 10 | %pre 11 | %code< 12 | :preserve 13 | cd ~/ 14 | mkdir -p local 15 | wget http://sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz 16 | tar -xzf sphinx-0.9.8.1.tar.gz 17 | cd sphinx-0.9.8.1/ 18 | ./configure --prefix=$HOME/local/ --exec-prefix=$HOME/local/ 19 | make 20 | install 21 | 22 | :textile 23 | Now you simply need to modify your $PATH to include ~/local/bin: 24 | 25 | %pre 26 | %code< 27 | :preserve 28 | echo "export PATH=\"$PATH:~/local/bin\"" >> ~/.bash_profile 29 | source ~/.bash_profile 30 | 31 | :textile 32 | Now you should have a working searchd and your only remaining concern is how to keeping it running, as Dreamhost will kill off long running processes. My solution was to add it to crontab: 33 | 34 | %pre 35 | %code< 36 | :preserve 37 | * * * * * /home/you/local/bin/searchd --config /home/you/path/sphinx.conf 38 | 39 | :textile 40 | This appears to be "a grey area":http://blog.dreamhosters.com/kbase/index.cgi?area=2449 with Dreamhost, but I've never really seen sphinx use any real CPU or memory (on my sites at least) and personally I think they should make it available as part of their standard build. -------------------------------------------------------------------------------- /articles/enter_sinatra.haml: -------------------------------------------------------------------------------- 1 | -# title: Enter Sinatra 2 | -# published: 2009-02-21 3 | 4 | :textile 5 | "Sinatra":http://www.sinatrarb.com/, I absolutely love it. After watching "toolmantim":http://toolmantim.com/ present on the topic at the last RORO I borrowed some inspiration (and "some code":http://github.com/toolmantim/toolmantim/tree/master) and came up with the new "hughevans.net":http://hughevans.net. 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | require 'rubygems' 11 | require 'sinatra' 12 | require 'haml' 13 | require 'time' 14 | require 'lib/article' 15 | 16 | Article.path = File.join(Sinatra::Application.root, 'articles') 17 | 18 | helpers do 19 | def article_body(article) 20 | haml(article.template, :layout => false) 21 | end 22 | 23 | def article_path(article) 24 | "/\#{article.published.strftime("%Y/%m/%d")}/\#{article.id}" 25 | end 26 | end 27 | 28 | get '/' do 29 | @articles = Article.all.sort[0..4] 30 | haml :home 31 | end 32 | 33 | get '/:year/:month/:day/:id' do 34 | @article = Article[params[:id]] || raise(Sinatra::NotFound) 35 | @single_view = true 36 | haml :article 37 | end 38 | 39 | get '/articles.atom' do 40 | @articles = Article.all.sort 41 | content_type 'application/atom+xml' 42 | haml :feed, :layout => false 43 | end 44 | 45 | get '/:style.css' do 46 | content_type 'text/css', :charset => 'utf-8' 47 | sass :"stylesheets/\#{params[:style]}" 48 | end 49 | 50 | :textile 51 | Nothing too clever, just simple clean ruby code. The full source is available on "GitHub":http://github.com/hughevans/hughevans.net/tree/master. -------------------------------------------------------------------------------- /lib/article.rb: -------------------------------------------------------------------------------- 1 | # Code originally from toolmantim: 2 | # http://github.com/toolmantim/toolmantim/blob/972372615c534916a3a1c8ae0f01e72626ca83e0/lib/article.rb 3 | class Article 4 | def self.path=(path) 5 | @path = path 6 | end 7 | 8 | def self.path(article_slug = nil) 9 | article_slug ? File.join(@path, "#{article_slug}.haml") : @path 10 | end 11 | 12 | def self.files 13 | Dir["#{File.expand_path(Article.path)}/*.haml"] 14 | end 15 | 16 | def self.all 17 | self.files.map {|f| new(f, File.read(f))} 18 | end 19 | 20 | def self.[](id) 21 | path = path(id.gsub('-', '_')) 22 | File.exist?(path) && new(path, File.read(path)) 23 | end 24 | 25 | def self.template_variable(text, name) 26 | text[/\-\s*#\s*#{name}:\s*(.+)/, 1] 27 | end 28 | 29 | def self.parse_date(date_string) 30 | date_string && Time.local(*date_string.split('-').map {|s| s.to_i}) 31 | end 32 | 33 | attr_reader :path, :template 34 | 35 | def initialize(file_path, file_contents) 36 | @path = file_path 37 | @template = file_contents 38 | end 39 | 40 | def slug 41 | File.basename(self.path, '.haml') 42 | end 43 | 44 | def id 45 | slug.gsub('_', '-') 46 | end 47 | 48 | def title 49 | template_variable('title') 50 | end 51 | 52 | def published 53 | @published ||= self.class.parse_date(template_variable('published')) 54 | end 55 | 56 | def updated 57 | @updated ||= self.class.parse_date(template_variable('updated')) 58 | end 59 | 60 | def last_modified 61 | updated || published 62 | end 63 | 64 | def template_variable(name) 65 | self.class.template_variable(self.template, name) 66 | end 67 | 68 | def <=>(other) 69 | [other.published.year, other.published.month, other.published.day] <=> [self.published.year, self.published.month, self.published.day] 70 | end 71 | end -------------------------------------------------------------------------------- /articles/translate_legacy_attributes.haml: -------------------------------------------------------------------------------- 1 | -# title: Translate Legacy Attribute Names 2 | -# published: 2009-08-20 3 | 4 | :textile 5 | Recently I've been working a lot with legacy databases, wrapping them with rails apps and exposing them to REST actions. One thing that annoyed me was the terribly inconsistent column naming conventions I came across. So I used *alias_attribute* to give them nicer names, but when it came to running *to_xml* on the models I found I needed to specify which attributes to leave out and use the alias's instead. Not so dry. So this is what I came up with: 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | class Candidate < Legacy::Base 11 | set_table_name 'CandidateData' 12 | set_primary_key 'Fileno' 13 | 14 | @@translations = { 15 | 'Fileno' => 'id', 16 | 'Familyname' => 'last_name', 17 | 'GivenNames' => 'first_name', 18 | 'DOB' => 'date_of_birth', 19 | 'Emailaddr' => 'email', 20 | 'Address' => 'address', 21 | 'City' => 'city', 22 | 'State' => 'state', 23 | 'PostCode' => 'postal_code', 24 | 'PhoneHome' => 'phone_home', 25 | 'PhoneWork' => 'phone_work', 26 | 'mobile' => 'phone_mobile' 27 | } 28 | 29 | @@translations.each {|k,v| alias_attribute(v, k)} 30 | 31 | alias_method :ar_to_xml, :to_xml 32 | 33 | def to_xml(options = {}, &block) 34 | default_options = { 35 | :except => @@translations.keys, 36 | :methods => @@translations.values 37 | } 38 | self.ar_to_xml(options.merge(default_options), &block) 39 | end 40 | 41 | :textile 42 | Also you only have to translate the attributes that need it as they will come through to *to_xml* as normal if not included in the translation hash. -------------------------------------------------------------------------------- /articles/sinatra_on_dreamhost.haml: -------------------------------------------------------------------------------- 1 | -# title: Sinatra 0.9 on Dreamhost 2 | -# published: 2009-02-22 3 | 4 | :textile 5 | There is "lots":http://www.sinatrarb.com/book.html#deployment of "good":http://railstips.org/2008/12/15/deploying-sinatra-on-dreamhost-with-passenger information out there on how to deploy Sinatra apps to Dreamhost but they don't really cover off the Sinatara 0.9 release which unfortunately depends on a newer version of the Rack gem that dreamhost does not yet have installed. The "missing piece of the puzzle":http://brianc.me/blog/sinatra-on-dreamhost/ was that you can actually override the Rack that Passenger loads by vendoring Rack and requiring it in your config.ru file. 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | require 'rubygems' 11 | require 'vendor/rack/lib/rack' 12 | require 'vendor/sinatra/lib/sinatra' 13 | 14 | disable :run 15 | 16 | set :app_file, 'yourapp.rb' 17 | set :views, '/full/path/views' 18 | 19 | require 'yourapp' 20 | run Sinatra::Application 21 | 22 | :textile 23 | Now just to tidy things up and prevent me from having junk in my repository I updated my Capistrano script to gem install Sinatra/Rack and unpack them to /vendor on deploy:setup. 24 | 25 | %pre 26 | %code.ruby< 27 | :preserve 28 | after 'deploy:setup', 'vendor_gems:install_and_unpack' 29 | after 'deploy:update_code', 'vendor_gems:symlink' 30 | 31 | namespace :vendor_gems do 32 | task :install_and_unpack do 33 | run 'gem install sinatra -v 0.9.0.4' # Also installs rack 0.9.1 34 | run "cd \#{shared_path}/system && gem unpack rack && mv rack-* rack" 35 | run "cd \#{shared_path}/system && gem unpack sinatra && mv sinatra-* sinatra" 36 | end 37 | 38 | task :symlink do 39 | run "mkdir -p \#{release_path}/vendor/" 40 | run "ln -nfs \#{shared_path}/system/rack \#{release_path}/vendor/rack" 41 | run "ln -nfs \#{shared_path}/system/sinatra \#{release_path}/vendor/sinatra" 42 | end 43 | end 44 | 45 | :textile 46 | Now I am a pretty happy camper as my move back to shared hosting seems to be going quite well so far. Thanks "Passenger":http://www.modrails.com/! 47 | 48 | :textile 49 | PS. If you're interested in trying Dreamhost you should know that they have a pretty generous affiliate program. You can use my "affiliate link":http://www.dreamhost.com/r.cgi?508625 or a friends, but either way you should make sure someone benefits from it when you signup. -------------------------------------------------------------------------------- /articles/jquery_toggler.haml: -------------------------------------------------------------------------------- 1 | -# title: jQuery Toggler 2 | -# published: 2008-05-28 3 | 4 | :textile 5 | I often like to have small form actions in my sidebar hidden away with javascript that are just a toggle away. Using jQuery to do this is quite easy but there are a few gotchas. Firstly we want both the always visible toggler link and a cancel button inside the toggled div to do the toggling. Next we want the first form input in the toggled div to become focused when we make it visible. It is important we don't try and do this when we are toggling the div off (hiding it) as Internet Explorer will bork when you try and focus on a hidden element. 6 | 7 | :textile 8 | Starting with our HTML: 9 | 10 | %pre 11 | %code.html< 12 | :preserve 13 |
14 |

Create New Category

15 |
16 |
17 |

18 | 19 | 20 |

21 |

22 | 23 | or 24 | Cancel 25 |

26 |
27 |
28 |
29 | 30 | :textile 31 | Now the JS: 32 | 33 | %pre 34 | %code.javascript< 35 | :preserve 36 | //// 37 | // Behaviours 38 | $(document).ready(function() { 39 | 40 | //// 41 | // Automatically hide all togglable elements on load 42 | $('.togglable').hide(); 43 | 44 | //// 45 | // Toggle a .togglable div inside the clicked parent .toggle div 46 | $('a.toggler').click(function() { 47 | var togglable = $(this).parents('.toggle').children('.togglable') 48 | togglable.toggle(); 49 | if(togglable.is(':visible')) { 50 | togglable.find('input:visible:enabled:first').focus(); 51 | }; 52 | return false; 53 | }); 54 | 55 | }); 56 | 57 | :textile 58 | The sense in this is that I traverse up to the parent div '.toggle' before filtering down to '.togglable', this way I can click both the 'h2 a' and the '.togglable a' to get the same effect. Also then I make sure that I don't try and focus on a hidden element because just doing 'input:visible:enabled:first' won't check if any of it's parents are hidden. 59 | 60 | :textile 61 | See the "Demo Page":http://hughevans.net/assets/jquery_toggler.html. -------------------------------------------------------------------------------- /articles/tasteful_routes.haml: -------------------------------------------------------------------------------- 1 | -# title: Tasteful Routes 2 | -# published: 2011-05-30 3 | 4 | :textile 5 | I was browsing Github today when I noticed the URL pattern they had employed for "Pull Requests 2.0":https://github.com/blog/712-pull-requests-2-0. The index is at /pulls as you'd expect, however the show action was in the format of /pull/1. Here's an "example":https://github.com/rails/rails/pull/1389. I actually think it's rather nice and reads better than the Rails default of /pulls/1. 6 | :textile 7 | Here is how to corral Rails into giving you these types of links for a collection and a nested collection (excuse the Ruby 1.9 hash syntax): 8 | 9 | %pre 10 | %code.ruby< 11 | :preserve 12 | match 'jobs' => 'jobs#index', as: :jobs 13 | match 'jobs' => 'jobs#create', via: :post 14 | 15 | resources :job, controller: 'jobs', except: [:index, :create] do 16 | match 'items' => 'items#index', as: :items 17 | match 'items' => 'items#create', via: :post 18 | resources :item, controller: 'items', except: [:index, :create] 19 | end 20 | 21 | :textile 22 | This gives you the following: 23 | 24 | %pre 25 | %code< 26 | :preserve 27 | $ rake routes 28 | jobs GET /jobs(.:format) {:action=>"index", :controller=>"jobs"} 29 | POST /jobs(.:format) {:action=>"create", :controller=>"jobs"} 30 | job GET /job/:id(.:format) {:action=>"show", :controller=>"jobs"} 31 | edit_job GET /job/:id/edit(.:format) {:action=>"edit", :controller=>"jobs"} 32 | PUT /job/:id(.:format) {:action=>"update", :controller=>"jobs"} 33 | DELETE /job/:id(.:format) {:action=>"destroy", :controller=>"jobs"} 34 | new_job GET /job/new(.:format) {:action=>"new", :controller=>"jobs"} 35 | job_items GET /job/:job_id/items(.:format) {:action=>"index", :controller=>"items"} 36 | POST /job/:job_id/items(.:format) {:action=>"create", :controller=>"items"} 37 | job_item GET /job/:job_id/item/:id(.:format) {:action=>"show", :controller=>"items"} 38 | edit_job_item GET /job/:job_id/item/:id/edit(.:format) {:action=>"edit", :controller=>"items"} 39 | PUT /job/:job_id/item/:id(.:format) {:action=>"update", :controller=>"items"} 40 | DELETE /job/:job_id/item/:id(.:format) {:action=>"destroy", :controller=>"items"} 41 | new_job_item GET /job/:job_id/item/new(.:format) {:action=>"new", :controller=>"items"} 42 | -------------------------------------------------------------------------------- /views/stylesheets/screen.sass: -------------------------------------------------------------------------------- 1 | body 2 | background-color: white 3 | color: black 4 | margin: 0 5 | padding: 0 6 | text-align: center 7 | font-family: "Helvetica Neue", Helvetica, arial, "sans-serif" 8 | font-size: 14px 9 | 10 | .wrapper 11 | width: 800px 12 | margin: 0 auto 13 | padding: 0 14 | text-align: left 15 | 16 | a 17 | color: #c31c1c 18 | text-decoration: none 19 | 20 | a:hover 21 | text-decoration: underline 22 | 23 | p 24 | line-height: 23px 25 | text-align: justify 26 | margin: 20px 0 27 | 28 | pre 29 | background-color: #232323 30 | border: 1px solid black 31 | padding: 10px 32 | margin: 20px 0 30px 33 | overflow: auto 34 | margin: 20px 0 35 | 36 | pre code 37 | font-family: 'Droid Sans Mono', 'Monaco', 'Courier New', 'Terminal', monospace 38 | font-size: 13px 39 | color: #e6e0db 40 | 41 | span.code 42 | font-family: 'Droid Sans Mono', 'Monaco', 'Courier New', 'Terminal', monospace 43 | background-color: #F8F8F8 44 | border: 1px solid #DCDCDC 45 | color: #9E9E9E 46 | padding: 0 47 | font-size: 12px 48 | 49 | .article 50 | margin: 0 0 40px 51 | h1 52 | font-family: "Helvetica Neue", Helvetica, arial, serif 53 | font-size: 26px 54 | &.title 55 | margin: 5px 0 56 | a 57 | color: black 58 | text-decoration: underline 59 | h2 60 | font-size: 22px 61 | .published 62 | font-family: 'Courier New', 'Terminal', monospace 63 | margin: 0 0 15px 0 64 | font-weight: bold 65 | 66 | #disqus_thread 67 | margin: 0 0 40px 68 | 69 | #main 70 | margin: 0 71 | padding: 0 72 | 73 | #header 74 | padding: 20px 0 0 75 | height: 80px 76 | h1 77 | font-family: 'Copse', Helvetica, arial, serif 78 | font-size: 28px 79 | 80 | #footer 81 | clear: both 82 | background-color: black 83 | color: #e6e0db 84 | padding: 20px 0 85 | .wrapper 86 | text-align: right 87 | a 88 | color: #e6e0db 89 | text-decoration: underline 90 | font-weight: normal 91 | a:hover 92 | color: #c31c1c 93 | ul 94 | margin: 0 95 | padding: 0 0 0 5px 96 | display: inline 97 | li 98 | margin: 0 99 | padding: 0 100 | display: inline 101 | list-style-image: none 102 | list-style-position: outside 103 | list-style-type: none 104 | li:after 105 | content: " | " 106 | color: #dbdadb 107 | li:last-child:after 108 | content: "" 109 | 110 | .clearfix 111 | clear: both 112 | 113 | a img 114 | border: 0 115 | 116 | code span 117 | &.comment 118 | color: #bc9458 119 | font-style: italic 120 | &.string 121 | color: #a5c261 122 | &.symbol 123 | color: #6d9cbe 124 | &.keywords 125 | color: #cc7833 126 | 127 | code.html span 128 | color: #ffc66d 129 | &.tag 130 | color: #ffc66d 131 | &.attribute 132 | color: #ffc66d 133 | &.string 134 | color: #a5c261 135 | -------------------------------------------------------------------------------- /articles/date_range_string_conversions.haml: -------------------------------------------------------------------------------- 1 | -# title: Rails Date Range String Conversions 2 | -# published: 2008-09-25 3 | 4 | :textile 5 | It has been widely publicised that you should use string conversions to display dates and times in rails, but there is another object type that can make use of these handy conversions and that is date ranges. I honestly didn't realise Ruby had date ranges until recently and I have to say they are pretty cool. Anyway onto my first date range conversion: 6 | 7 | %pre 8 | %code.ruby< 9 | :preserve 10 | ActiveSupport::CoreExtensions::Range::Conversions::RANGE_FORMATS.merge!( 11 | :long => Proc.new { |start, stop| "\#{start.to_date.to_s(:dmy_long)} to \#{stop.to_date.to_s(:dmy_long)}" } 12 | ) 13 | 14 | :textile 15 | Using a Proc you can pick up on the first and last date from the range and concatenate them into the string format of your choosing. I've used a date string conversion there aswell: 16 | 17 | %pre 18 | %code.ruby< 19 | :preserve 20 | ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!( 21 | :dmy_long => Proc.new { |date| "\#{date.day.ordinalize} \#{date.strftime '%B %Y'}" } 22 | ) 23 | 24 | :textile 25 | With these conversions loaded you can do the following: 26 | 27 | %pre 28 | %code< 29 | :preserve 30 | >> date_range = 20.days.from_now..30.days.from_now 31 | => Wed, 15 Oct 2008 11:09:03 UTC +00:00..Sat, 25 Oct 2008 11:09:03 UTC +00:00 32 | >> date_range.to_s(:long) 33 | => "15th October 2008 to 25th October 2008" 34 | 35 | :textile 36 | Pretty nice but if you want to make it a little dry-er in terms of the output like '15-25th October 2008' you could do something like this: 37 | 38 | %pre 39 | %code.ruby< 40 | :preserve 41 | ActiveSupport::CoreExtensions::Range::Conversions::RANGE_FORMATS.merge!( 42 | :condensed => Proc.new do |start, stop| 43 | if (start.year == stop.year) and (start.month == stop.month) 44 | "\#{start.day}-\#{stop.to_date.to_s(:dmy_long)}" 45 | else 46 | "\#{start.to_date.to_s(:dmy_long)} to \#{stop.to_date.to_s(:dmy_long)}" 47 | end 48 | end 49 | ) 50 | 51 | :textile 52 | So when the date range is limited to the one month it will give you the condensed version: 53 | 54 | %pre 55 | %code< 56 | :preserve 57 | >> date_range = 20.days.from_now..30.days.from_now 58 | => Wed, 15 Oct 2008 11:35:13 UTC +00:00..Sat, 25 Oct 2008 11:35:13 UTC +00:00 59 | >> date_range.to_s(:condensed) 60 | => "15-25th October 2008" 61 | 62 | :textile 63 | And if the range spans more than one month it jumps back to the full version: 64 | 65 | %pre 66 | %code< 67 | :preserve 68 | >> date_range = 20.days.from_now..50.days.from_now 69 | => Wed, 15 Oct 2008 11:35:22 UTC +00:00..Fri, 14 Nov 2008 11:35:22 UTC +00:00 70 | >> date_range.to_s(:condensed) 71 | => "15th October 2008 to 14th November 2008" 72 | 73 | :textile 74 | Of course don't forget the more logic you put in these conversions the greater the need for some tests. -------------------------------------------------------------------------------- /public/assets/jquery_toggler.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | jQuery Toggler 7 | 8 | 34 | 58 | 59 | 60 | 94 | 95 | -------------------------------------------------------------------------------- /public/javascripts/code_highlighter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Rev 146 from: 3 | http://svn.danwebb.net/external/CodeHighlighter/trunk/ 4 | */ 5 | 6 | /* Unobtrustive Code Highlighter By Dan Webb 11/2005 7 | Version: 0.4 8 | 9 | Usage: 10 | Add a script tag for this script and any stylesets you need to use 11 | to the page in question, add correct class names to CODE elements, 12 | define CSS styles for elements. That's it! 13 | 14 | Known to work on: 15 | IE 5.5+ PC 16 | Firefox/Mozilla PC/Mac 17 | Opera 7.23 + PC 18 | Safari 2 19 | 20 | Known to degrade gracefully on: 21 | IE5.0 PC 22 | 23 | Note: IE5.0 fails due to the use of lookahead in some stylesets. To avoid script errors 24 | in older browsers use expressions that use lookahead in string format when defining stylesets. 25 | 26 | This script is inspired by star-light by entirely cunning Dean Edwards 27 | http://dean.edwards.name/star-light/. 28 | */ 29 | 30 | // replace callback support for safari. 31 | if ("a".replace(/a/, function() {return "b"}) != "b") (function(){ 32 | var default_replace = String.prototype.replace; 33 | String.prototype.replace = function(search,replace){ 34 | // replace is not function 35 | if(typeof replace != "function"){ 36 | return default_replace.apply(this,arguments) 37 | } 38 | var str = "" + this; 39 | var callback = replace; 40 | // search string is not RegExp 41 | if(!(search instanceof RegExp)){ 42 | var idx = str.indexOf(search); 43 | return ( 44 | idx == -1 ? str : 45 | default_replace.apply(str,[search,callback(search, idx, str)]) 46 | ) 47 | } 48 | var reg = search; 49 | var result = []; 50 | var lastidx = reg.lastIndex; 51 | var re; 52 | while((re = reg.exec(str)) != null){ 53 | var idx = re.index; 54 | var args = re.concat(idx, str); 55 | result.push( 56 | str.slice(lastidx,idx), 57 | callback.apply(null,args).toString() 58 | ); 59 | if(!reg.global){ 60 | lastidx += RegExp.lastMatch.length; 61 | break 62 | }else{ 63 | lastidx = reg.lastIndex; 64 | } 65 | } 66 | result.push(str.slice(lastidx)); 67 | return result.join("") 68 | } 69 | })(); 70 | 71 | var CodeHighlighter = { styleSets : new Array }; 72 | 73 | CodeHighlighter.addStyle = function(name, rules) { 74 | // using push test to disallow older browsers from adding styleSets 75 | if ([].push) this.styleSets.push({ 76 | name : name, 77 | rules : rules, 78 | ignoreCase : arguments[2] || false 79 | }) 80 | 81 | function setEvent() { 82 | // set highlighter to run on load (use LowPro if present) 83 | if (typeof Event != 'undefined' && typeof Event.onReady == 'function') 84 | return Event.onReady(CodeHighlighter.init.bind(CodeHighlighter)); 85 | 86 | var old = window.onload; 87 | 88 | if (typeof window.onload != 'function') { 89 | window.onload = function() { CodeHighlighter.init() }; 90 | } else { 91 | window.onload = function() { 92 | old(); 93 | CodeHighlighter.init(); 94 | } 95 | } 96 | } 97 | 98 | // only set the event when the first style is added 99 | if (this.styleSets.length==1) setEvent(); 100 | } 101 | 102 | CodeHighlighter.init = function() { 103 | if (!document.getElementsByTagName) return; 104 | if ("a".replace(/a/, function() {return "b"}) != "b") return; // throw out Safari versions that don't support replace function 105 | // throw out older browsers 106 | 107 | var codeEls = document.getElementsByTagName("CODE"); 108 | // collect array of all pre elements 109 | codeEls.filter = function(f) { 110 | var a = new Array; 111 | for (var i = 0; i < this.length; i++) if (f(this[i])) a[a.length] = this[i]; 112 | return a; 113 | } 114 | 115 | var rules = new Array; 116 | rules.toString = function() { 117 | // joins regexes into one big parallel regex 118 | var exps = new Array; 119 | for (var i = 0; i < this.length; i++) exps.push(this[i].exp); 120 | return exps.join("|"); 121 | } 122 | 123 | function addRule(className, rule) { 124 | // add a replace rule 125 | var exp = (typeof rule.exp != "string")?String(rule.exp).substr(1, String(rule.exp).length-2):rule.exp; 126 | // converts regex rules to strings and chops of the slashes 127 | rules.push({ 128 | className : className, 129 | exp : "(" + exp + ")", 130 | length : (exp.match(/(^|[^\\])\([^?]/g) || "").length + 1, // number of subexps in rule 131 | replacement : rule.replacement || null 132 | }); 133 | } 134 | 135 | function parse(text, ignoreCase) { 136 | // main text parsing and replacement 137 | return text.replace(new RegExp(rules, (ignoreCase)?"gi":"g"), function() { 138 | var i = 0, j = 1, rule; 139 | while (rule = rules[i++]) { 140 | if (arguments[j]) { 141 | // if no custom replacement defined do the simple replacement 142 | if (!rule.replacement) return "" + arguments[0] + ""; 143 | else { 144 | // replace $0 with the className then do normal replaces 145 | var str = rule.replacement.replace("$0", rule.className); 146 | for (var k = 1; k <= rule.length - 1; k++) str = str.replace("$" + k, arguments[j + k]); 147 | return str; 148 | } 149 | } else j+= rule.length; 150 | } 151 | }); 152 | } 153 | 154 | function highlightCode(styleSet) { 155 | // clear rules array 156 | var parsed, clsRx = new RegExp("(\\s|^)" + styleSet.name + "(\\s|$)"); 157 | rules.length = 0; 158 | 159 | // get stylable elements by filtering out all code elements without the correct className 160 | var stylableEls = codeEls.filter(function(item) { return clsRx.test(item.className) }); 161 | 162 | // add style rules to parser 163 | for (var className in styleSet.rules) addRule(className, styleSet.rules[className]); 164 | 165 | 166 | // replace for all elements 167 | for (var i = 0; i < stylableEls.length; i++) { 168 | // EVIL hack to fix IE whitespace badness if it's inside a
169 |       if (/MSIE/.test(navigator.appVersion) && stylableEls[i].parentNode.nodeName == 'PRE') {
170 |         stylableEls[i] = stylableEls[i].parentNode;
171 |         
172 |         parsed = stylableEls[i].innerHTML.replace(/(]*>)([^<]*)<\/code>/i, function() {
173 |           return arguments[1] + parse(arguments[2], styleSet.ignoreCase) + ""
174 |         });
175 |         parsed = parsed.replace(/\n( *)/g, function() { 
176 |           var spaces = "";
177 |           for (var i = 0; i < arguments[1].length; i++) spaces+= " ";
178 |           return "\n" + spaces;  
179 |         });
180 |         parsed = parsed.replace(/\t/g, "    ");
181 |         parsed = parsed.replace(/\n(<\/\w+>)?/g, "
$1").replace(/
[\n\r\s]*
/g, "


"); 182 | 183 | } else parsed = parse(stylableEls[i].innerHTML, styleSet.ignoreCase); 184 | 185 | stylableEls[i].innerHTML = parsed; 186 | } 187 | } 188 | 189 | // run highlighter on all stylesets 190 | for (var i=0; i < this.styleSets.length; i++) { 191 | highlightCode(this.styleSets[i]); 192 | } 193 | } 194 | 195 | 196 | 197 | CodeHighlighter.addStyle("css", { 198 | comment : { 199 | exp : /\/\*[^*]*\*+([^\/][^*]*\*+)*\// 200 | }, 201 | keywords : { 202 | exp : /@\w[\w\s]*/ 203 | }, 204 | selectors : { 205 | exp : "([\\w-:\\[.#][^{};>]*)(?={)" 206 | }, 207 | properties : { 208 | exp : "([\\w-]+)(?=\\s*:)" 209 | }, 210 | units : { 211 | exp : /([0-9])(em|en|px|%|pt)\b/, 212 | replacement : "$1$2" 213 | }, 214 | urls : { 215 | exp : /url\([^\)]*\)/ 216 | } 217 | }); 218 | 219 | CodeHighlighter.addStyle("ruby",{ 220 | comment : { 221 | exp : /#[^\n]+/ 222 | }, 223 | brackets : { 224 | exp : /\(|\)/ 225 | }, 226 | string : { 227 | exp : /'[^']*'|"[^"]*"/ 228 | }, 229 | keywords : { 230 | exp : /\b(do|end|self|class|def|if|module|yield|then|else|for|until|unless|while|elsif|case|when|break|retry|redo|rescue|require|raise)\b/ 231 | }, 232 | /* Added by Shelly Fisher (shelly@agileevolved.com) */ 233 | symbol : { 234 | exp : /([^:])(:[A-Za-z0-9_!?]+)/ 235 | } 236 | }); 237 | 238 | CodeHighlighter.addStyle("javascript",{ 239 | comment : { 240 | exp : /(\/\/[^\n]*\n)|(\/\*[^*]*\*+([^\/][^*]*\*+)*\/)/ 241 | }, 242 | brackets : { 243 | exp : /\(|\)/ 244 | }, 245 | string : { 246 | exp : /'[^']*'|"[^"]*"/ 247 | }, 248 | keywords : { 249 | exp : /\b(arguments|break|case|continue|default|delete|do|else|false|for|function|if|in|instanceof|new|null|return|switch|this|true|typeof|var|void|while|with)\b/ 250 | }, 251 | global : { 252 | exp : /\b(toString|valueOf|window|element|prototype|constructor|document|escape|unescape|parseInt|parseFloat|setTimeout|clearTimeout|setInterval|clearInterval|NaN|isNaN|Infinity)\b/ 253 | } 254 | }); 255 | 256 | CodeHighlighter.addStyle("html", { 257 | comment : { 258 | exp: /<!\s*(--([^-]|[\r\n]|-[^-])*--\s*)>/ 259 | }, 260 | tag : { 261 | exp: /(<\/?)([a-zA-Z]+\s?)/, 262 | replacement: "$1$2" 263 | }, 264 | string : { 265 | exp : /'[^']*'|"[^"]*"/ 266 | }, 267 | attribute : { 268 | exp: /\b([a-zA-Z-:]+)(=)/, 269 | replacement: "$1$2" 270 | }, 271 | doctype : { 272 | exp: /<!DOCTYPE([^&]|&[^g]|&g[^t])*>/ 273 | } 274 | }); 275 | 276 | CodeHighlighter.addStyle("shell",{ 277 | keywords : { 278 | exp: /(\$ ?)([^\n]+)/ 279 | } 280 | }); --------------------------------------------------------------------------------