├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── app.rb ├── helpers.rb ├── helpers │ ├── pretty_printing.rb │ └── sinatra.rb ├── libraries.rb ├── models │ ├── db.rb │ └── user.rb ├── templates │ ├── 404.mustache │ ├── 500.mustache │ ├── about.mustache │ ├── hurls.mustache │ ├── index.mustache │ ├── layout.mustache │ ├── stats.mustache │ └── view.mustache └── views │ ├── about.rb │ ├── hurls.rb │ ├── index.rb │ ├── layout.rb │ ├── stats.rb │ └── view.rb ├── config.ru ├── docs └── index.mustache ├── hurls.yaml ├── public ├── .gitignore ├── css │ ├── facebox.css │ ├── jquery.autocomplete.css │ ├── pygment_trac.css │ ├── reset.css │ └── style.css ├── favicon.ico ├── img │ ├── ajax-loader.gif │ ├── autocomplete-loader.gif │ ├── background.jpg │ ├── buttonbg.png │ ├── delete.png │ ├── facebox │ │ ├── b.png │ │ ├── bl.png │ │ ├── br.png │ │ ├── closelabel.gif │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── shadow.gif │ │ ├── tl.png │ │ └── tr.png │ ├── favicon.gif │ ├── logo.png │ ├── message-info.png │ ├── message-warn.png │ ├── pony-hurl.png │ ├── pony.png │ ├── rainbow.png │ └── unicorn.png ├── js │ ├── facebox.js │ ├── hurl.headers.js │ ├── hurl.js │ ├── jquery.autocomplete.js │ ├── jquery.form.js │ ├── jquery.js │ ├── jquery.relatize_date.js │ └── json2.js └── robots.txt ├── test ├── json └── xml └── tmp └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | .bundle/ 3 | bin/ 4 | env.rb -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'sinatra', '~>1.0' 4 | gem 'yajl-ruby', '~>1.1.0' 5 | gem 'mustache', '~>0.11.2' 6 | gem 'curb', '~>0.7.8' 7 | gem 'coderay', '~>0.8.357' 8 | 9 | gem 'sinatra_auth_github' 10 | 11 | group :test do 12 | gem 'shotgun' 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.2.2) 5 | coderay (0.8.357) 6 | curb (0.7.8) 7 | faraday (0.4.6) 8 | addressable (>= 2.1.1) 9 | rack (>= 1.0.1) 10 | json (1.4.6) 11 | mime-types (1.16) 12 | multi_json (0.0.5) 13 | mustache (0.11.2) 14 | oauth2 (0.0.13) 15 | faraday (~> 0.4.1) 16 | multi_json (>= 0.0.4) 17 | rack (1.2.1) 18 | rest-client (1.5.1) 19 | mime-types (>= 1.16) 20 | shotgun (0.8) 21 | rack (>= 1.0) 22 | sinatra (1.1.0) 23 | rack (~> 1.1) 24 | tilt (~> 1.1) 25 | sinatra_auth_github (0.0.11) 26 | rest-client (~> 1.5.1) 27 | sinatra (~> 1.0) 28 | warden-github (~> 0.0.5) 29 | tilt (1.1) 30 | warden (0.10.7) 31 | rack (>= 1.0.0) 32 | warden-github (0.0.6) 33 | json (>= 1.0.0) 34 | oauth2 (~> 0.0.8) 35 | warden (~> 0.10) 36 | yajl-ruby (1.1.0) 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | coderay (~> 0.8.357) 43 | curb (~> 0.7.8) 44 | mustache (~> 0.11.2) 45 | shotgun 46 | sinatra (~> 1.0) 47 | sinatra_auth_github 48 | yajl-ruby (~> 1.1.0) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Chris Wanstrath and Leah Culver 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hurl 2 | ==== 3 | 4 | Hurl was created for the Rails Rumble 2009 in 48 hours. 5 | Now Hurl is an open source project for your enjoyment. 6 | 7 | 8 | 9 | 10 | Installation 11 | ------------ 12 | 13 | Hurl requires Ruby 1.8.6+ 14 | 15 | First download hurl and cd into the directory: 16 | 17 | git clone git://github.com/twilio/hurl 18 | cd hurl 19 | 20 | Or download [the zip](http://github.com/twilio/hurl/zipball/master). 21 | 22 | Next make sure you have [RubyGems](https://rubygems.org/pages/download) installed. 23 | 24 | Then install [Bundler](http://gembundler.com/): 25 | 26 | gem install bundler 27 | 28 | Now install Hurl's dependencies: 29 | 30 | bundle install 31 | 32 | 33 | Run Locally 34 | ----------- 35 | 36 | bundle exec shotgun config.ru 37 | 38 | Now visit 39 | 40 | 41 | Issues 42 | ------ 43 | 44 | Find a bug? Want a feature? Submit an [issue 45 | here](http://github.com/twilio/hurl/issues). Patches welcome! 46 | 47 | 48 | Screenshot 49 | ---------- 50 | 51 | [![Hurl](http://img.skitch.com/20091020-xtiqtj4eajuxs43iu5h3be7upj.png)](http://hurl.it) 52 | 53 | 54 | Original Authors 55 | ---------------- 56 | 57 | * [Leah Culver][2] 58 | * [Chris Wanstrath][3] 59 | 60 | 61 | [1]: http://r09.railsrumble.com/ 62 | [2]: http://github.com/leah 63 | [3]: http://github.com/defunkt 64 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | namespace :hurl do 2 | desc "Start Hurl for development" 3 | task :start do 4 | exec "bundle exec shotgun config.ru" 5 | end 6 | 7 | desc "Generate GitHub pages." 8 | task :pages => :check_dirty do 9 | require "mustache" 10 | require "rdiscount" 11 | view = Mustache.new 12 | view.template = File.read("docs/index.mustache") 13 | view[:content] = Markdown.new(File.read("README.md")).to_html 14 | File.open("new_index.html", "w") do |f| 15 | f.puts view.render 16 | end 17 | system "git checkout gh-pages" 18 | system "git pull origin gh-pages" 19 | system "mv new_index.html index.html" 20 | system "git commit -a -m 'auto update docs'" 21 | system "git push origin gh-pages" 22 | system "git checkout master" 23 | end 24 | 25 | task :check_dirty do 26 | if `git status -a` && $?.success? 27 | abort "dirty index - not publishing!" 28 | end 29 | end 30 | 31 | desc "Please pardon our dust." 32 | task :deploy do 33 | exec "ssh deploy@hurl.it 'cd /www/hurl && git fetch origin && git reset --hard origin/master && rake bundle && touch tmp/restart.txt'" 34 | end 35 | end 36 | 37 | # Needs bundler, uglifyjs, and uglifycss installed on the server. 38 | # 39 | # Bundler: 40 | # gem install bundler 41 | # uglifyjs: 42 | # npm install uglify-js 43 | # uglifycss: 44 | # curl https://github.com/fmarcia/UglifyCSS/raw/master/uglifycss > /usr/bin/uglifcss 45 | task :bundle do 46 | system "bundle install" 47 | rm "public/js/bundle.js" rescue nil # >:O 48 | rm "public/css/bundle.css" rescue nil 49 | system "cat $(ls -1 public/js/*.js | grep -v jquery.js) | uglifyjs -nc > public/js/bundle.js" 50 | system "uglifycss public/css/*.css > public/css/bundle.css" 51 | end 52 | 53 | desc "Start everything." 54 | multitask :start => [ 'hurl:start' ] 55 | -------------------------------------------------------------------------------- /app/app.rb: -------------------------------------------------------------------------------- 1 | require 'app/libraries' 2 | 3 | module Hurl 4 | class App < Sinatra::Base 5 | register Mustache::Sinatra 6 | helpers Hurl::Helpers 7 | 8 | dir = File.dirname(File.expand_path(__FILE__)) 9 | 10 | set :public, "#{dir}/public" 11 | set :root, RACK_ROOT 12 | set :app_file, __FILE__ 13 | set :static, true 14 | 15 | set :views, "#{dir}/templates" 16 | 17 | set :mustache, { 18 | :namespace => Object, 19 | :views => "#{dir}/views", 20 | :templates => "#{dir}/templates" 21 | } 22 | 23 | enable :sessions 24 | 25 | set :github_options, { :client_id => ENV['HURL_CLIENT_ID'], 26 | :secret => ENV['HURL_SECRET'], 27 | :scopes => '', 28 | :callback_url => '/login/callback/' } 29 | 30 | register ::Sinatra::Auth::Github 31 | 32 | def initialize(*args) 33 | super 34 | @debug = ENV['DEBUG'] 35 | @ga_code = ENV['GA_CODE'] 36 | setup_default_hurls 37 | end 38 | 39 | 40 | # 41 | # routes 42 | # 43 | 44 | before do 45 | if authenticated? 46 | @user = User.new(github_user) 47 | end 48 | 49 | @flash = session.delete('flash') 50 | end 51 | 52 | get '/' do 53 | @hurl = params 54 | mustache :index 55 | end 56 | 57 | get '/hurls/?' do 58 | redirect('/') and return unless logged_in? 59 | @hurls = @user.hurls 60 | mustache :hurls 61 | end 62 | 63 | get '/hurls/:id/?' do 64 | @hurl = find_hurl_or_view(params[:id]) 65 | @hurl ? mustache(:index) : not_found 66 | end 67 | 68 | delete '/hurls/:id/?' do 69 | redirect('/') and return unless logged_in? 70 | 71 | if @hurl = find_hurl_or_view(params[:id]) 72 | @user.remove_hurl(@hurl['id']) 73 | end 74 | request.xhr? ? "ok" : redirect('/') 75 | end 76 | 77 | get '/hurls/:id/:view_id/?' do 78 | @hurl = find_hurl_or_view(params[:id]) 79 | @view = find_hurl_or_view(params[:view_id]) 80 | @view_id = params[:view_id] 81 | @hurl && @view ? mustache(:index) : not_found 82 | end 83 | 84 | get '/views/:id/?' do 85 | @view = find_hurl_or_view(params[:id]) 86 | @view ? mustache(:view, :layout => false) : not_found 87 | end 88 | 89 | get '/test.json' do 90 | content_type 'application/json' 91 | File.read('test/json') 92 | end 93 | 94 | get '/test.xml' do 95 | content_type 'application/xml' 96 | File.read('test/xml') 97 | end 98 | 99 | get '/about/?' do 100 | mustache :about 101 | end 102 | 103 | get '/stats/?' do 104 | mustache :stats 105 | end 106 | 107 | get '/login/?' do 108 | authenticate! 109 | redirect '/' 110 | end 111 | 112 | get '/login/callback/?' do 113 | authenticate! 114 | redirect '/' 115 | end 116 | 117 | get '/logout/?' do 118 | logout! 119 | session['flash'] = 'see you later!' 120 | redirect '/' 121 | end 122 | 123 | post '/' do 124 | return json(:error => "Calm down and try my margarita!") if rate_limited? 125 | 126 | url, method, auth = params.values_at(:url, :method, :auth) 127 | 128 | return json(:error => "That's... wait.. what?!") if invalid_url?(url) 129 | 130 | curl = Curl::Easy.new(url) 131 | 132 | sent_headers = [] 133 | curl.on_debug do |type, data| 134 | # track request headers 135 | sent_headers << data if type == Curl::CURLINFO_HEADER_OUT 136 | end 137 | 138 | curl.follow_location = true if params[:follow_redirects] 139 | 140 | # ensure a method is set 141 | method = (method.to_s.empty? ? 'GET' : method).upcase 142 | 143 | # update auth 144 | add_auth(auth, curl, params) 145 | 146 | # arbitrary headers 147 | add_headers_from_arrays(curl, params["header-keys"], params["header-vals"]) 148 | 149 | # arbitrary post params 150 | if params['post-body'] && ['POST', 'PUT'].index(method) 151 | post_data = [params['post-body']] 152 | else 153 | post_data = make_fields(method, params["param-keys"], params["param-vals"]) 154 | end 155 | 156 | begin 157 | debug { puts "#{method} #{url}" } 158 | 159 | if method == 'PUT' 160 | curl.http_put(stringify_data(post_data)) 161 | else 162 | curl.send("http_#{method.downcase}", *post_data) 163 | end 164 | 165 | debug do 166 | puts sent_headers.join("\n") 167 | puts post_data.join('&') if post_data.any? 168 | puts curl.header_str 169 | end 170 | 171 | header = pretty_print_headers(curl.header_str) 172 | type = url =~ /(\.js)$/ ? 'js' : curl.content_type 173 | body = pretty_print(type, curl.body_str) 174 | request = pretty_print_requests(sent_headers, post_data) 175 | 176 | json :header => header, 177 | :body => body, 178 | :request => request, 179 | :hurl_id => save_hurl(params), 180 | :prev_hurl => @user ? @user.second_to_last_hurl_id : nil, 181 | :view_id => save_view(header, body, request) 182 | rescue => e 183 | json :error => CGI::escapeHTML(e.to_s) 184 | end 185 | end 186 | 187 | 188 | # 189 | # error handlers 190 | # 191 | 192 | not_found do 193 | mustache :"404" 194 | end 195 | 196 | error do 197 | mustache :"500" 198 | end 199 | 200 | 201 | # 202 | # route helpers 203 | # 204 | 205 | # is this a url hurl can handle. basically a spam check. 206 | def invalid_url?(url) 207 | valid_schemes = ['http', 'https'] 208 | begin 209 | uri = URI.parse(url) 210 | raise URI::InvalidURIError if uri.host == 'hurl.it' 211 | raise URI::InvalidURIError if !valid_schemes.include? uri.scheme 212 | false 213 | rescue URI::InvalidURIError 214 | true 215 | end 216 | end 217 | 218 | # update auth based on auth type 219 | def add_auth(auth, curl, params) 220 | if auth == 'basic' 221 | username, password = params.values_at(:username, :password) 222 | encoded = Base64.encode64("#{username}:#{password}").gsub("\n",'') 223 | curl.headers['Authorization'] = "Basic #{encoded}" 224 | end 225 | end 226 | 227 | # headers from non-empty keys and values 228 | def add_headers_from_arrays(curl, keys, values) 229 | keys, values = Array(keys), Array(values) 230 | 231 | keys.each_with_index do |key, i| 232 | next if values[i].to_s.empty? 233 | curl.headers[key] = values[i] 234 | end 235 | end 236 | 237 | # post params from non-empty keys and values 238 | def make_fields(method, keys, values) 239 | return [] unless %w( POST PUT ).include? method 240 | 241 | fields = [] 242 | keys, values = Array(keys), Array(values) 243 | keys.each_with_index do |name, i| 244 | value = values[i] 245 | next if name.to_s.empty? || value.to_s.empty? 246 | fields << Curl::PostField.content(name, value) 247 | end 248 | fields 249 | end 250 | 251 | def save_view(header, body, request) 252 | hash = { 'header' => header, 'body' => body, 'request' => request } 253 | id = sha(hash.to_s) 254 | DB.save(:views, id, hash) 255 | id 256 | end 257 | 258 | def save_hurl(params) 259 | id = sha(params.to_s) 260 | DB.save(:hurls, id, params.merge(:id => id)) 261 | @user.add_hurl(id) if @user 262 | id 263 | end 264 | 265 | def find_hurl_or_view(id) 266 | DB.find(:hurls, id) || DB.find(:views, id) 267 | end 268 | 269 | # has this person made too many requests? 270 | def rate_limited? 271 | false 272 | end 273 | 274 | # turn post_data into a string for PUT requests 275 | def stringify_data(data) 276 | if data.is_a? String 277 | data 278 | elsif data.is_a? Array 279 | data.map { |x| stringify_data(x) }.join("&") 280 | elsif data.is_a? Curl::PostField 281 | data.to_s 282 | else 283 | raise "Cannot stringify #{data.inspect}" 284 | end 285 | end 286 | end 287 | end 288 | -------------------------------------------------------------------------------- /app/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'helpers/pretty_printing' 2 | require 'helpers/sinatra' 3 | require 'yaml' 4 | 5 | module Hurl 6 | module Helpers 7 | include Hurl::Helpers::PrettyPrinting 8 | include Hurl::Helpers::Sinatra 9 | 10 | # 11 | # helpers defined here are available to all views and sinatra routes 12 | # 13 | 14 | # debug { puts "hi!" } 15 | def debug 16 | yield if @debug 17 | end 18 | 19 | # sha(hash) => '01578ad840f1a7eba2bd202351119e635fde8e2a' 20 | def sha(thing) 21 | Digest::SHA1.hexdigest(thing.to_s) 22 | end 23 | 24 | def logged_in? 25 | !!@user 26 | end 27 | 28 | def ga_code 29 | @ga_code 30 | end 31 | 32 | def user 33 | @user 34 | end 35 | 36 | # for sorting hashes with symbol keys 37 | def sort_hash(hash) 38 | hash.to_a.sort_by { |a, b| a.to_s } 39 | end 40 | 41 | def next_hurl 42 | return unless logged_in? 43 | 44 | if @hurl 45 | hurls = @user.hurls 46 | hurls.each_with_index do |hurl, i| 47 | if hurl['id'] == @hurl['id'] 48 | return i-1 >= 0 ? hurls[i-1]['id'] : nil 49 | end 50 | end 51 | nil 52 | end 53 | end 54 | 55 | def prev_hurl 56 | return unless logged_in? 57 | 58 | if @hurl.empty? && @user.hurls.any? 59 | @user.latest_hurl_id 60 | elsif @hurl.any? 61 | hurls = @user.hurls 62 | hurls.each_with_index do |hurl, i| 63 | if hurl['id'] == @hurl['id'] 64 | return hurls[i+1] ? hurls[i+1]['id'] : nil 65 | end 66 | end 67 | nil 68 | end 69 | end 70 | 71 | # creates the hurls shown on the front page if they're not in the db 72 | def setup_default_hurls 73 | default_hurls.each do |name, params| 74 | save_hurl(params) 75 | end 76 | end 77 | 78 | def default_hurls 79 | return @default_hurls if @default_hurls 80 | path = File.expand_path(App.root + '/hurls.yaml') 81 | @default_hurls = YAML.load_file(path) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /app/helpers/pretty_printing.rb: -------------------------------------------------------------------------------- 1 | module Hurl 2 | module Helpers 3 | # Pretty printing of content types for the response div. 4 | module PrettyPrinting 5 | def pretty_print(type, content) 6 | type = type.to_s 7 | 8 | if type =~ /json|javascript/ 9 | pretty_print_json(content) 10 | elsif type == 'js' 11 | pretty_print_js(content) 12 | elsif type.include? 'xml' 13 | pretty_print_xml(content) 14 | elsif type.include? 'html' 15 | colorize :html => content 16 | else 17 | CGI::escapeHTML(content.inspect) 18 | end 19 | end 20 | 21 | def pretty_print_json(content) 22 | json = Yajl::Parser.parse(content) 23 | pretty_print_js Yajl::Encoder.new(:pretty => true).encode(json) 24 | end 25 | 26 | def pretty_print_js(content) 27 | colorize :js => content 28 | end 29 | 30 | def pretty_print_xml(content) 31 | out = StringIO.new 32 | doc = REXML::Document.new(content) 33 | doc.write(out, 2) 34 | colorize :xml => out.string 35 | end 36 | 37 | def pretty_print_headers(content) 38 | content = CGI::escapeHTML(content) 39 | lines = content.split("\n").map do |line| 40 | if line =~ /^(.+?):(.+)$/ 41 | "#{$1}:#{$2}" 42 | else 43 | "#{line}" 44 | end 45 | end 46 | 47 | "
#{lines.join}
" 48 | end 49 | 50 | # accepts an array of request headers and formats them 51 | def pretty_print_requests(requests = [], fields = []) 52 | headers = requests.map do |request| 53 | pretty_print_headers request 54 | end 55 | 56 | headers.join + fields.join('&') 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/helpers/sinatra.rb: -------------------------------------------------------------------------------- 1 | module Hurl 2 | module Helpers 3 | # Random Sinatra DSL helpers. 4 | # Quite generic. 5 | module Sinatra 6 | # render a json response 7 | def json(hash = {}) 8 | content_type 'application/json' 9 | Yajl::Encoder.encode(hash) 10 | end 11 | 12 | # colorize :js => '{ "blah": true }' 13 | def colorize(hash = {}) 14 | tokens = CodeRay.scan(hash.values.first, hash.keys.first) 15 | colored = tokens.html.div.sub('CodeRay', 'highlight') 16 | colored.gsub(/(https?:\/\/[^< "']+)/, '\1') 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/libraries.rb: -------------------------------------------------------------------------------- 1 | RACK_ENV = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development' 2 | RACK_ROOT = File.expand_path(File.dirname(__FILE__) + '/..') 3 | 4 | # std lib 5 | require 'open3' 6 | require 'uri' 7 | require 'base64' 8 | require 'digest' 9 | require 'zlib' 10 | require "rexml/document" 11 | 12 | # bundled gems 13 | require 'sinatra/base' 14 | require 'yajl' 15 | require 'curb' 16 | require 'mustache/sinatra' 17 | require 'sinatra/auth/github' 18 | require 'coderay' 19 | 20 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) 21 | 22 | # hurl 23 | require 'helpers' 24 | 25 | require 'models/db' 26 | require 'models/user' 27 | 28 | require 'views/layout' 29 | -------------------------------------------------------------------------------- /app/models/db.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Hurl 4 | class DB 5 | DIR = File.expand_path(ENV['HURL_DB_DIR'] || "db") 6 | 7 | def self.find(scope, id) 8 | decode File.read(dir(scope, id) + id) if id && id.is_a?(String) 9 | rescue Errno::ENOENT 10 | nil 11 | end 12 | 13 | def self.save(scope, id, content) 14 | File.open(dir(scope, id) + id, 'w') do |f| 15 | f.puts encode(content) 16 | end 17 | 18 | true 19 | end 20 | 21 | def self.dir(scope, id) 22 | path = FileUtils.mkdir_p "#{DIR}/#{scope}/#{id[0...2]}/#{id[2...4]}/" 23 | 24 | # In Ruby 1.9, mkdir_p always returns Array, 25 | # while in 1.8 it returns String when it has only one item to return. 26 | if path.is_a? Array 27 | path[0] 28 | else 29 | path 30 | end 31 | end 32 | 33 | private 34 | def self.encode(object) 35 | Zlib::Deflate.deflate Yajl::Encoder.encode(object) 36 | end 37 | 38 | def self.decode(object) 39 | Yajl::Parser.parse(Zlib::Inflate.inflate(object)) rescue nil 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | module Hurl 2 | class User 3 | attr_accessor :id, :login, :github_user 4 | 5 | def initialize(github_user) 6 | @github_user = github_user 7 | @login = github_user.login 8 | @id = github_user.attribs['id'] 9 | end 10 | 11 | # 12 | # each user has an associated list 13 | # of hurls 14 | # 15 | 16 | def latest_hurl 17 | hurls(0, 1).first 18 | end 19 | 20 | def second_to_last_hurl_id 21 | hurls.any? and hurls(0, 2).size == 2 and hurls(0, 2)[1]['id'] 22 | end 23 | 24 | def latest_hurl_id 25 | hurls.any? and latest_hurl['id'] 26 | end 27 | 28 | def add_hurl(id) 29 | hurl_ids = DB.find(:users, db_id) || {} 30 | hurl_ids[id] = Time.now 31 | DB.save(:users, db_id, hurl_ids) 32 | end 33 | 34 | def remove_hurl(id) 35 | hurl_ids = DB.find(:users, db_id) || {} 36 | hurl_ids.delete(id) 37 | DB.save(:users, db_id, hurl_ids) 38 | end 39 | 40 | def db_id 41 | Digest::MD5.hexdigest(id.to_s) 42 | end 43 | 44 | def unsorted_hurls(start = 0, limit = 100) 45 | Array(DB.find(:users, db_id)).map do |id, date| 46 | DB.find(:hurls, id).merge('date' => Time.parse(date)) if id 47 | end.compact 48 | end 49 | 50 | def hurls(start = 0, limit = 100) 51 | unsorted_hurls(start, limit).sort_by { |h| -h['date'].to_i } 52 | end 53 | 54 | # 55 | # instance methods 56 | # 57 | 58 | def to_s 59 | login 60 | end 61 | 62 | def gravatar_url 63 | "http://www.gravatar.com/avatar/%s" % github_user.attribs['gravatar_id'] 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/templates/404.mustache: -------------------------------------------------------------------------------- 1 |

404 not found

2 |
3 | 4 |

This is not the hurl you are looking for.

5 |

Go home.

6 |
-------------------------------------------------------------------------------- /app/templates/500.mustache: -------------------------------------------------------------------------------- 1 |

500 error

2 |
3 | 4 |

Something went very wrong.

5 |

Go home.

6 |
-------------------------------------------------------------------------------- /app/templates/about.mustache: -------------------------------------------------------------------------------- 1 |

Hurl?

2 |
3 |

Hurl makes HTTP requests.

4 |

Choose the request method, customize headers and POST parameters, add basic authorization, and even follow redirects. 5 | Then view the nicely formatted request and response.

6 |

It's the perfect tool for testing APIs. Just enter a URL and click send.

7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |

Hurl was created by Chris Wanstrath and Leah Culver 20 | for the 2009 Rails Rumble.

21 |

Hurl is now maintained by Twilio

22 |

Follow Hurl on Twitter: @hurlit

23 |

Grab the source code: http://github.com/twilio/hurl

24 |
25 |

... and it's kind of like cURL.

26 |
-------------------------------------------------------------------------------- /app/templates/hurls.mustache: -------------------------------------------------------------------------------- 1 |

Your hurls

2 | {{# any_hurls?}} 3 | 4 | {{# hurls}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{/ hurls}} 13 |
{{ url }}{{ method }}{{ auth }}{{ date }}
14 | {{/ any_hurls?}} 15 | 16 | {{^ any_hurls?}} 17 |
18 | 19 |

We found some hurls, but none are yours.

20 |
21 | {{/ any_hurls?}} 22 | -------------------------------------------------------------------------------- /app/templates/index.mustache: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{# previous_hurl}} 4 | 5 | {{/ previous_hurl}} 6 | 7 | {{^ previous_hurl}} 8 | 9 | {{/ previous_hurl}} 10 | 11 | {{# next_hurl}} 12 | 13 | {{/ next_hurl}} 14 | 15 | {{^ next_hurl}} 16 | {{# hurl?}} 17 | {{# logged_in?}} 18 | 19 | {{/ logged_in?}} 20 | 21 | {{^ logged_in?}} 22 | 23 | {{/ logged_in?}} 24 | {{/ hurl?}} 25 | {{/ next_hurl}} 26 | 27 | 28 |

29 |

30 | 37 | 38 | 42 | 43 |

66 |

67 |

68 | 72 | 80 |

84 |

85 |

86 | + add header 87 |

88 | 89 | {{# hurl_header_keys }} 90 |

91 | 92 | 93 | 94 |

95 | {{/ hurl_header_keys }} 96 | 97 | {{# no_hurl_header_keys }} 98 | 103 | {{/ no_hurl_header_keys }} 104 | 105 |

106 | 107 | 108 |

109 |
110 |
111 |
112 |

Hurl makes HTTP requests.

113 |

Enter a URL, set some headers, view the response, then share it with others.

114 |

Perfect for demoing and debugging APIs.

115 |
116 |
117 |
118 |
119 |

Try it out:

120 |
    121 | {{# default_hurls }} 122 |
  • {{ name }}
  • 123 | {{/ default_hurls }} 124 |
125 |
126 |
127 |
128 |
129 | view full size 130 | 131 |
132 |

133 | 136 | 137 | request response 138 | 139 |

140 | 145 |
146 |
147 | {{# view }} 148 |
{{{ header }}}
{{{ body }}} 149 | {{/ view }} 150 |
151 |
152 |
153 | view full size 154 | 155 |
156 |
157 | -------------------------------------------------------------------------------- /app/templates/layout.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hurl.it - Test HTTP API requests - Twilio Labs 5 | 6 | 7 | {{# bundled?}} 8 | 9 | 10 | 11 | {{/ bundled?}} 12 | 13 | {{^ bundled?}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{/ bundled?}} 29 | 30 | 31 | 32 |
33 | 51 |
52 | 56 |

57 | 58 | {{ flash }} 59 |

60 | {{{ yield }}} 61 |
62 | 69 |
70 | {{# ga_code }} 71 | 82 | {{/ ga_code }} 83 | 84 | -------------------------------------------------------------------------------- /app/templates/stats.mustache: -------------------------------------------------------------------------------- 1 |
2 |

Stats

3 | 4 | 5 | {{# hurl_stats}} 6 | 7 | 8 | 9 | 10 | {{/ hurl_stats}} 11 |
{{ stat }}{{ value }}
12 | 13 |

Disk

14 | 15 | 16 | {{# disk_stats}} 17 | 18 | 19 | 20 | 21 | {{/ disk_stats}} 22 |
{{ stat }}{{ value }}
23 | 24 |

25 | Deployed SHA: 26 | {{ deployed_sha }} 27 |

28 |
29 | -------------------------------------------------------------------------------- /app/templates/view.mustache: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | hurl 6 | 7 | 8 | 16 | 17 | 18 | {{{ view_request }}} 19 | {{{ view_header }}} 20 | {{{ view_body }}} 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/views/about.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class About < Layout 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/views/hurls.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Hurls < Layout 3 | def hurls 4 | @user.hurls.map do |hurl| 5 | hurl['auth'] = hurl['auth'] == 'none' ? 'no auth' : 'HTTP basic' 6 | hurl 7 | end 8 | end 9 | 10 | def any_hurls? 11 | @user.hurls.any? 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/index.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Index < Layout 3 | def previous_hurl 4 | if prev = prev_hurl 5 | [ :hurl => prev.to_s ] 6 | end 7 | end 8 | 9 | def next_hurl 10 | if nxt = super 11 | [ :hurl => nxt.to_s ] 12 | end 13 | end 14 | 15 | def help_blurb_hidden? 16 | logged_in? or not @hurl.empty? 17 | end 18 | 19 | def try_it_hidden? 20 | not @hurl.empty? 21 | end 22 | 23 | def default_hurls 24 | super.sort.map do |name, params| 25 | dname = name.downcase 26 | { :name => name, :sha => sha(params), :class => dname.split(' ')[0] } 27 | end 28 | end 29 | 30 | def hide_request_and_response? 31 | @view.nil? 32 | end 33 | 34 | 35 | # 36 | # @hurl related 37 | # 38 | 39 | def hurl? 40 | @hurl && @hurl.any? 41 | end 42 | 43 | def hurl_post_body 44 | @hurl['post-body'] if @hurl 45 | end 46 | 47 | def hurl_url 48 | @hurl['url'] if @hurl 49 | end 50 | 51 | def method_is_GET? 52 | @hurl['method'] == 'GET' 53 | end 54 | 55 | def method_is_HEAD? 56 | @hurl['method'] == 'HEAD' 57 | end 58 | 59 | def method_is_POST? 60 | @hurl['method'] == 'POST' 61 | end 62 | 63 | def method_is_PUT? 64 | @hurl['method'] == 'PUT' 65 | end 66 | 67 | def method_is_DELETE? 68 | @hurl['method'] == 'DELETE' 69 | end 70 | 71 | def hurl_param_keys 72 | return if @hurl['param-keys'].nil? 73 | arr = [] 74 | @hurl['param-keys'].each_with_index do |name, i| 75 | arr << { :name => name, :value => @hurl['param-vals'][i] } 76 | end 77 | arr 78 | end 79 | 80 | def no_hurl_param_keys 81 | hurl_param_keys.nil? || hurl_param_keys.empty? 82 | end 83 | 84 | def hurl_header_keys 85 | return if @hurl['header-keys'].nil? 86 | arr = [] 87 | @hurl['header-keys'].each_with_index do |name, i| 88 | arr << { :name => name, :value => @hurl['header-vals'][i] } 89 | end 90 | arr 91 | end 92 | 93 | def no_hurl_header_keys 94 | hurl_header_keys.nil? || hurl_header_keys.empty? 95 | end 96 | 97 | def hurl_basic_auth? 98 | @hurl['auth'] == 'basic' 99 | end 100 | 101 | def hurl_username 102 | @hurl['username'] 103 | end 104 | 105 | def hurl_password 106 | @hurl['password'] 107 | end 108 | 109 | def hurl_permalink 110 | @view_id ? "/hurls/#{@hurl['id']}/#{@view_id}" : "#" 111 | end 112 | 113 | def follows_redirects? 114 | @hurl['follows_redirects'] 115 | end 116 | 117 | 118 | # 119 | # view related 120 | # 121 | 122 | def view_permalink 123 | @view_id ? "/views/#{@view_id}" : "#" 124 | end 125 | 126 | def view_request 127 | @view['request'] if @view 128 | end 129 | 130 | def view 131 | return unless @view 132 | [ :header => @view['header'], :body => @view['body'] ] 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /app/views/layout.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Layout < Mustache 3 | include Hurl::Helpers 4 | 5 | def no_flash 6 | not @flash 7 | end 8 | 9 | def flash 10 | @flash 11 | end 12 | 13 | def deployed_sha 14 | @deployed_sha ||= `git rev-parse --short HEAD`.chomp 15 | end 16 | 17 | def bundled? 18 | File.exist?("#{RACK_ROOT}/public/css/bundle.css") && 19 | File.exist?("#{RACK_ROOT}/public/js/bundle.js") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/stats.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Stats < Layout 3 | def hurl_stats 4 | return [ 5 | count(:users), 6 | count(:views), 7 | count(:hurls) 8 | ] 9 | end 10 | 11 | def count(thing) 12 | files = Dir["#{Hurl::DB::DIR}/#{thing}/**/**"].reject do |file| 13 | File.directory?(file) 14 | end 15 | 16 | { :stat => thing, :value => files.size } 17 | end 18 | 19 | def disk_stats 20 | [ :stat => 'db-size', :value => `du -sh db`.split(' ')[0] ] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/view.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class View < Layout 3 | def view_request 4 | @view['request'] 5 | end 6 | 7 | def view_header 8 | @view['header'] 9 | end 10 | 11 | def view_body 12 | @view['body'] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | unless $LOAD_PATH.include? "." 2 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) 3 | end 4 | 5 | begin 6 | require './env' 7 | rescue LoadError 8 | nil 9 | end 10 | 11 | require 'app/app' 12 | 13 | run Hurl::App.new 14 | -------------------------------------------------------------------------------- /docs/index.mustache: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | hurl 8 | 9 | 10 | 11 | 12 |
13 | 14 | Fork me on GitHub 15 | 16 |
17 |

18 | hurl 19 | by Twilio 20 |

21 | 22 |
23 | Hurl makes HTTP requests. 24 |
25 | 26 | {{{ content }}} 27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /hurls.yaml: -------------------------------------------------------------------------------- 1 | # The default Hurls shown on the home page 2 | Twilio API (view account): 3 | url: https://api.twilio.com/2010-04-01/Accounts 4 | method: GET 5 | auth: basic 6 | username: account_sid_here 7 | password: auth_token_here 8 | 9 | Twitter API (public timeline): 10 | url: http://twitter.com/statuses/public_timeline.json 11 | method: GET 12 | auth: none 13 | 14 | Vimeo API (video): 15 | url: http://vimeo.com/api/v2/video/6238577.xml 16 | method: GET 17 | auth: none 18 | 19 | GitHub API (repository): 20 | url: http://github.com/api/v2/json/repos/show/twilio/hurl 21 | method: GET 22 | auth: none -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/.gitignore -------------------------------------------------------------------------------- /public/css/facebox.css: -------------------------------------------------------------------------------- 1 | #facebox .b { 2 | background:url(/img/facebox/b.png); 3 | } 4 | 5 | #facebox .tl { 6 | background:url(/img/facebox/tl.png); 7 | } 8 | 9 | #facebox .tr { 10 | background:url(/img/facebox/tr.png); 11 | } 12 | 13 | #facebox .bl { 14 | background:url(/img/facebox/bl.png); 15 | } 16 | 17 | #facebox .br { 18 | background:url(/img/facebox/br.png); 19 | } 20 | 21 | #facebox { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | z-index: 100; 26 | text-align: left; 27 | } 28 | 29 | #facebox .popup { 30 | position: relative; 31 | } 32 | 33 | #facebox table { 34 | border-collapse: collapse; 35 | } 36 | 37 | #facebox td { 38 | border-bottom: 0; 39 | padding: 0; 40 | } 41 | 42 | #facebox .body { 43 | padding: 10px; 44 | background: #fff; 45 | width: 370px; 46 | } 47 | 48 | #facebox .loading { 49 | text-align: center; 50 | } 51 | 52 | #facebox .image { 53 | text-align: center; 54 | } 55 | 56 | #facebox img { 57 | border: 0; 58 | margin: 0; 59 | } 60 | 61 | #facebox .footer { 62 | border-top: 1px solid #DDDDDD; 63 | padding-top: 5px; 64 | margin-top: 10px; 65 | text-align: right; 66 | } 67 | 68 | #facebox .footer img { 69 | vertical-align: middle; 70 | } 71 | 72 | #facebox .tl, #facebox .tr, #facebox .bl, #facebox .br { 73 | height: 10px; 74 | width: 10px; 75 | overflow: hidden; 76 | padding: 0; 77 | } 78 | 79 | #facebox_overlay { 80 | position: fixed; 81 | top: 0px; 82 | left: 0px; 83 | height:100%; 84 | width:100%; 85 | } 86 | 87 | .facebox_hide { 88 | z-index:-100; 89 | } 90 | 91 | .facebox_overlayBG { 92 | background-color: #000; 93 | z-index: 99; 94 | } 95 | 96 | * html #facebox_overlay { /* ie6 hack */ 97 | position: absolute; 98 | height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); 99 | } -------------------------------------------------------------------------------- /public/css/jquery.autocomplete.css: -------------------------------------------------------------------------------- 1 | .ac_results { 2 | padding: 0px; 3 | border: 1px solid WindowFrame; 4 | background-color: Window; 5 | overflow: hidden; 6 | } 7 | 8 | .ac_results ul { 9 | width: 100%; 10 | list-style-position: outside; 11 | list-style: none; 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | .ac_results iframe { 17 | display:none;/*sorry for IE5*/ 18 | display/**/:block;/*sorry for IE5*/ 19 | position:absolute; 20 | top:0; 21 | left:0; 22 | z-index:-1; 23 | filter:mask(); 24 | width:3000px; 25 | height:3000px; 26 | } 27 | 28 | .ac_results li { 29 | margin: 0px; 30 | padding: 2px 5px; 31 | cursor: pointer; 32 | display: block; 33 | width: 100%; 34 | font: menu; 35 | font-size: 12px; 36 | overflow: hidden; 37 | } 38 | 39 | .ac_loading { 40 | background : Window url('/img/autocomplete-loader.gif') right center no-repeat; 41 | } 42 | 43 | .ac_over { 44 | background-color: Highlight; 45 | color: HighlightText; 46 | } 47 | -------------------------------------------------------------------------------- /public/css/pygment_trac.css: -------------------------------------------------------------------------------- 1 | /* .highlight { background: #ffffff; } */ 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .kw { font-weight: bold } /* Keyword */ 5 | .highlight .o { font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .highlight .pp { color: #999999; font-weight: bold } /* Comment.Preproc */ 9 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 11 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 12 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 13 | .highlight .ge { font-style: italic } /* Generic.Emph */ 14 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 15 | .highlight .gh { color: #999999 } /* Generic.Heading */ 16 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 17 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 18 | .highlight .go { color: #888888 } /* Generic.Output */ 19 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ 22 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 23 | .highlight .kc { font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 26 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 27 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 28 | .highlight .m { color: #009999 } /* Literal.Number */ 29 | .highlight .s { color: #d14 } /* Literal.String */ 30 | .highlight .na { color: #008080 } /* Name.Attribute */ 31 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 32 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 33 | .highlight .no { color: #008080 } /* Name.Constant */ 34 | .highlight .ni { color: #800080 } /* Name.Entity */ 35 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 36 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 37 | .highlight .nn { color: #555555 } /* Name.Namespace */ 38 | .highlight .nt { color: #000080 } /* Name.Tag */ 39 | .highlight .ta { color: #000080 } /* Name.Tag */ 40 | .highlight .nv { color: #008080 } /* Name.Variable */ 41 | .highlight .ow { font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 46 | .highlight .i { color: #009999 } /* Literal.Number.Integer */ 47 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 48 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 49 | .highlight .sc { color: #d14 } /* Literal.String.Char */ 50 | .highlight .sd { color: #d14 } /* Literal.String.Doc */ 51 | .highlight .s2 { color: #d14 } /* Literal.String.Double */ 52 | .highlight .k { color: #d14 } /* Literal.String.Double */ 53 | .highlight .se { color: #d14 } /* Literal.String.Escape */ 54 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 55 | .highlight .si { color: #d14 } /* Literal.String.Interpol */ 56 | .highlight .sx { color: #d14 } /* Literal.String.Other */ 57 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 58 | .highlight .s1 { color: #d14 } /* Literal.String.Single */ 59 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 60 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 61 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 62 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 63 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 64 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 65 | -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | html, body, div, span, applet, object, iframe, 3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 4 | a, abbr, acronym, address, big, cite, code, 5 | del, dfn, em, font, img, ins, kbd, q, s, samp, 6 | small, strike, strong, sub, sup, tt, var, 7 | dl, dt, dd, ul, li, 8 | form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | outline: 0; 14 | font-weight: inherit; 15 | font-style: normal; 16 | font-size: 100%; 17 | font-family: inherit; 18 | vertical-align: baseline; 19 | } 20 | :focus { 21 | outline: 0; 22 | } 23 | body { 24 | line-height: 1; 25 | color: black; 26 | background: white; 27 | } 28 | ul { 29 | list-style: none; 30 | } 31 | table { 32 | border-collapse: collapse; 33 | border-spacing: 0; 34 | } 35 | caption, th, td { 36 | text-align: left; 37 | font-weight: normal; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ""; 42 | } 43 | blockquote, q { 44 | quotes: "" ""; 45 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* general */ 2 | body { 3 | font: 1em "lucida Grande",arial,sans-serif; 4 | background: #fff url(/img/rainbow.png) repeat; 5 | color: #333; 6 | } 7 | p { 8 | margin: 6px 0; 9 | } 10 | a { 11 | text-decoration: none; 12 | color: #26B3CF; 13 | } 14 | a:hover { 15 | text-decoration: underline; 16 | } 17 | h2 { 18 | font-size: 1.5em; 19 | margin-bottom: 20px; 20 | } 21 | .clearwrap { 22 | overflow: hidden; 23 | } 24 | img.gravatar { 25 | width: 18px; 26 | height: 18px; 27 | padding: 1px; 28 | border: 1px solid #c0c0c0; 29 | } 30 | /* forms */ 31 | label { 32 | font-size: 16px; 33 | } 34 | select, 35 | input[type=text], 36 | input[type=password], 37 | textarea { 38 | -x-system-font:none; 39 | color:#333; 40 | font: 1em "lucida Grande",arial,sans-serif; 41 | font-size: 16px; 42 | font-size-adjust:none; 43 | font-stretch: normal; 44 | font-style: normal; 45 | font-variant: normal; 46 | font-weight: normal; 47 | line-height: normal; 48 | padding: 5px; 49 | border: 1px solid #bbb; 50 | width: 748px; 51 | margin: 2px 0; 52 | } 53 | select.form-alpha { 54 | width: 290px; 55 | } 56 | input[type=text].form-alpha { 57 | width: 280px; 58 | } 59 | .facebox input[type=text], 60 | .facebox input[type=password] { 61 | width: 380px; 62 | } 63 | input[type=text].form-beta, 64 | input[type=password].form-beta { 65 | width: 444px; 66 | } 67 | .form-beta { 68 | margin-left: 6px !important; 69 | } 70 | /* buttons */ 71 | button { 72 | font: 1em "Helvetica Neue", Arial, sans-serif; 73 | color: #333; 74 | padding: 0.365em 10px; 75 | line-height: 1.4em; 76 | font-weight: bold; 77 | cursor: pointer; 78 | background: #e9eaea url(/img/buttonbg.png) repeat-x; 79 | border: 1px solid #ccc; 80 | text-shadow: 1px 1px 0 #fff; 81 | -moz-border-radius: 2px; 82 | -webkit-border-radius: 2px; 83 | } 84 | button:hover { 85 | opacity: 0.8; 86 | } 87 | button:active { 88 | opacity: 1; 89 | } 90 | #send-wrap { 91 | height: 40px; 92 | margin-top: 16px; 93 | } 94 | #send-wrap img { 95 | padding: 0.8em 10px; 96 | line-height: 1.4em; 97 | } 98 | #spinner { 99 | padding-right: 9px 1px; 100 | display: none; 101 | } 102 | /* close buttons */ 103 | img.delete { 104 | opacity: 0.5; 105 | } 106 | .header-delete, 107 | .param-delete { 108 | float: right; 109 | margin-right: -20px; 110 | padding-top: 7px; 111 | } 112 | .flash-close { 113 | float: right; 114 | margin-right: -26px; 115 | } 116 | /* layout */ 117 | #inset { 118 | background-color: #fff; 119 | width: 860px; 120 | margin: 0 auto; 121 | padding-bottom: 40px; 122 | } 123 | #content { 124 | margin: 0 50px; 125 | } 126 | #header { 127 | padding: 20px 50px 5px 50px; 128 | border-bottom: 5px solid #26B3CF; 129 | margin-bottom: 30px; 130 | } 131 | #nav { 132 | float: right; 133 | margin-top: 34px; 134 | } 135 | a.nav-item { 136 | margin-right: 12px; 137 | } 138 | #footer { 139 | margin-top: 60px; 140 | background-color: #26B3CF; 141 | padding: 4px 50px; 142 | text-align: right; 143 | } 144 | #footer a { 145 | color: #fff; 146 | } 147 | /* homepage */ 148 | #page-prev, 149 | #page-next { 150 | font-weight: bold; 151 | margin-top: 7px; 152 | } 153 | #page-prev { 154 | float: left; 155 | margin-left: -30px; 156 | } 157 | #page-next { 158 | float: right; 159 | margin-right: -30px; 160 | } 161 | #page-prev:hover, 162 | #page-next:hover { 163 | text-decoration: none; 164 | } 165 | #hurl-form { 166 | margin: 20px 0; 167 | } 168 | #auth-selection, 169 | #header-selection { 170 | margin-top: 10px; 171 | } 172 | .utils { 173 | float: right; 174 | margin-top: 6px; 175 | } 176 | .link-icon { 177 | font-weight: bold; 178 | } 179 | /* flash */ 180 | .flash-error, 181 | .error-msg { 182 | padding: 6px 6px 6px 36px; 183 | background: #ef95a1 url(/img/message-warn.png) 6px 50% no-repeat; 184 | } 185 | .flash-notice { 186 | padding: 6px 6px 6px 36px; 187 | background: #86EF9E url(/img/message-info.png) 6px 50% no-repeat; 188 | } 189 | /* help text */ 190 | #help-text { 191 | text-align: center; 192 | background-color: #eee; 193 | margin: 20px 0; 194 | -moz-border-radius: 5px; 195 | -webkit-border-radius: 5px; 196 | padding: 10px 0; 197 | } 198 | ul.examples li { 199 | padding-left: 24px; 200 | margin: 3px 0; 201 | } 202 | ul.examples li.twitter { 203 | background: #fff url(http://twitter.com/favicon.ico) 0 40% no-repeat; 204 | } 205 | ul.examples li.vimeo { 206 | background: #fff url(http://vimeo.com/favicon.ico) 0 40% no-repeat; 207 | } 208 | ul.examples li.github { 209 | background: #fff url(http://github.com/favicon.ico) 0 40% no-repeat; 210 | } 211 | ul.examples li.twilio { 212 | background: #fff url(http://www.twilio.com/favicon.ico) 0 40% no-repeat; 213 | background-size: 16px 16px; 214 | } 215 | ul.examples li.baconfile { 216 | background: #fff url(http://baconfile.com/favicon.ico) 0 40% no-repeat; 217 | } 218 | /* request/response area */ 219 | .tab-current, 220 | .toggle-reqres-link { 221 | padding: 6px; 222 | margin-right: 4px; 223 | } 224 | .tab-current, 225 | .toggle-reqres-link:hover { 226 | background-color: #26B3CF; 227 | color: #fff; 228 | -moz-border-radius: 5px; 229 | -webkit-border-radius: 5px; 230 | text-decoration: none; 231 | } 232 | .code-wrap { 233 | margin-top: 14px; 234 | background-color: #eee; 235 | padding: 8px 16px; 236 | -moz-border-radius: 5px; 237 | -webkit-border-radius: 5px; 238 | } 239 | .code-text { 240 | padding: 2px; 241 | min-height: 60px; 242 | overflow-y: auto; 243 | font-family:'Bitstream Vera Sans Mono','Courier',monospace; 244 | font-size: 14px; 245 | } 246 | /* saved archives page */ 247 | table td { 248 | padding-right: 12px; 249 | padding-bottom: 6px; 250 | } 251 | td.date { 252 | color: #aaa; 253 | } 254 | /* about */ 255 | .blurb { 256 | margin: 20px 0; 257 | } 258 | .blurb p { 259 | margin: 10px 0; 260 | } 261 | #about-video { 262 | text-align: center; 263 | margin-bottom: 20px; 264 | } 265 | /* stats */ 266 | #stats h2 { 267 | color: #26B3CF; 268 | margin-bottom: 6px; 269 | } 270 | #stats table { 271 | margin-bottom: 15px; 272 | } 273 | /* error */ 274 | .unicorn { 275 | text-align: center; 276 | } 277 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/favicon.ico -------------------------------------------------------------------------------- /public/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/ajax-loader.gif -------------------------------------------------------------------------------- /public/img/autocomplete-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/autocomplete-loader.gif -------------------------------------------------------------------------------- /public/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/background.jpg -------------------------------------------------------------------------------- /public/img/buttonbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/buttonbg.png -------------------------------------------------------------------------------- /public/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/delete.png -------------------------------------------------------------------------------- /public/img/facebox/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/b.png -------------------------------------------------------------------------------- /public/img/facebox/bl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/bl.png -------------------------------------------------------------------------------- /public/img/facebox/br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/br.png -------------------------------------------------------------------------------- /public/img/facebox/closelabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/closelabel.gif -------------------------------------------------------------------------------- /public/img/facebox/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/loading.gif -------------------------------------------------------------------------------- /public/img/facebox/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/logo.png -------------------------------------------------------------------------------- /public/img/facebox/shadow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/shadow.gif -------------------------------------------------------------------------------- /public/img/facebox/tl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/tl.png -------------------------------------------------------------------------------- /public/img/facebox/tr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/facebox/tr.png -------------------------------------------------------------------------------- /public/img/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/favicon.gif -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/logo.png -------------------------------------------------------------------------------- /public/img/message-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/message-info.png -------------------------------------------------------------------------------- /public/img/message-warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/message-warn.png -------------------------------------------------------------------------------- /public/img/pony-hurl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/pony-hurl.png -------------------------------------------------------------------------------- /public/img/pony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/pony.png -------------------------------------------------------------------------------- /public/img/rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/rainbow.png -------------------------------------------------------------------------------- /public/img/unicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/hurl2/e28d7f58d38f70dccaca1a32d3771d44fdb9a597/public/img/unicorn.png -------------------------------------------------------------------------------- /public/js/facebox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Facebox (for jQuery) 3 | * version: 1.2 (05/05/2008) 4 | * @requires jQuery v1.2 or later 5 | * 6 | * Examples at http://famspam.com/facebox/ 7 | * 8 | * Licensed under the MIT: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * Copyright 2007, 2008 Chris Wanstrath [ chris@ozmm.org ] 12 | * 13 | * Usage: 14 | * 15 | * jQuery(document).ready(function() { 16 | * jQuery('a[rel*=facebox]').facebox() 17 | * }) 18 | * 19 | * Terms 20 | * Loads the #terms div in the box 21 | * 22 | * Terms 23 | * Loads the terms.html page in the box 24 | * 25 | * Terms 26 | * Loads the terms.png image in the box 27 | * 28 | * 29 | * You can also use it programmatically: 30 | * 31 | * jQuery.facebox('some html') 32 | * jQuery.facebox('some html', 'my-groovy-style') 33 | * 34 | * The above will open a facebox with "some html" as the content. 35 | * 36 | * jQuery.facebox(function($) { 37 | * $.get('blah.html', function(data) { $.facebox(data) }) 38 | * }) 39 | * 40 | * The above will show a loading screen before the passed function is called, 41 | * allowing for a better ajaxy experience. 42 | * 43 | * The facebox function can also display an ajax page, an image, or the contents of a div: 44 | * 45 | * jQuery.facebox({ ajax: 'remote.html' }) 46 | * jQuery.facebox({ ajax: 'remote.html' }, 'my-groovy-style') 47 | * jQuery.facebox({ image: 'stairs.jpg' }) 48 | * jQuery.facebox({ image: 'stairs.jpg' }, 'my-groovy-style') 49 | * jQuery.facebox({ div: '#box' }) 50 | * jQuery.facebox({ div: '#box' }, 'my-groovy-style') 51 | * 52 | * Want to close the facebox? Trigger the 'close.facebox' document event: 53 | * 54 | * jQuery(document).trigger('close.facebox') 55 | * 56 | * Facebox also has a bunch of other hooks: 57 | * 58 | * loading.facebox 59 | * beforeReveal.facebox 60 | * reveal.facebox (aliased as 'afterReveal.facebox') 61 | * init.facebox 62 | * 63 | * Simply bind a function to any of these hooks: 64 | * 65 | * $(document).bind('reveal.facebox', function() { ...stuff to do after the facebox and contents are revealed... }) 66 | * 67 | */ 68 | (function($) { 69 | $.facebox = function(data, klass) { 70 | $.facebox.loading() 71 | 72 | if (data.ajax) fillFaceboxFromAjax(data.ajax, klass) 73 | else if (data.image) fillFaceboxFromImage(data.image, klass) 74 | else if (data.div) fillFaceboxFromHref(data.div, klass) 75 | else if ($.isFunction(data)) data.call($) 76 | else $.facebox.reveal(data, klass) 77 | } 78 | 79 | /* 80 | * Public, $.facebox methods 81 | */ 82 | 83 | $.extend($.facebox, { 84 | settings: { 85 | opacity : 0, 86 | overlay : true, 87 | loadingImage : '/facebox/loading.gif', 88 | closeImage : '/facebox/closelabel.gif', 89 | imageTypes : [ 'png', 'jpg', 'jpeg', 'gif' ], 90 | faceboxHtml : '\ 91 | ' 118 | }, 119 | 120 | loading: function() { 121 | init() 122 | if ($('#facebox .loading').length == 1) return true 123 | showOverlay() 124 | 125 | $('#facebox .content').empty() 126 | $('#facebox .body').children().hide().end(). 127 | append('
') 128 | 129 | $('#facebox').css({ 130 | top: getPageScroll()[1] + (getPageHeight() / 10), 131 | left: $(window).width() / 2 - 205 132 | }).show() 133 | 134 | $(document).bind('keydown.facebox', function(e) { 135 | if (e.keyCode == 27) $.facebox.close() 136 | return true 137 | }) 138 | $(document).trigger('loading.facebox') 139 | }, 140 | 141 | reveal: function(data, klass) { 142 | $(document).trigger('beforeReveal.facebox') 143 | if (klass) $('#facebox .content').addClass(klass) 144 | $('#facebox .content').append(data) 145 | $('#facebox .loading').remove() 146 | $('#facebox .body').children().fadeIn('normal') 147 | $('#facebox').css('left', $(window).width() / 2 - ($('#facebox table').width() / 2)) 148 | $(document).trigger('reveal.facebox').trigger('afterReveal.facebox') 149 | }, 150 | 151 | close: function() { 152 | $(document).trigger('close.facebox') 153 | return false 154 | } 155 | }) 156 | 157 | /* 158 | * Public, $.fn methods 159 | */ 160 | 161 | $.fn.facebox = function(settings) { 162 | if ($(this).length == 0) return 163 | 164 | init(settings) 165 | 166 | function clickHandler() { 167 | $.facebox.loading(true) 168 | 169 | // support for rel="facebox.inline_popup" syntax, to add a class 170 | // also supports deprecated "facebox[.inline_popup]" syntax 171 | var klass = this.rel.match(/facebox\[?\.(\w+)\]?/) 172 | if (klass) klass = klass[1] 173 | 174 | fillFaceboxFromHref(this.href, klass) 175 | return false 176 | } 177 | 178 | return this.bind('click.facebox', clickHandler) 179 | } 180 | 181 | /* 182 | * Private methods 183 | */ 184 | 185 | // called one time to setup facebox on this page 186 | function init(settings) { 187 | if ($.facebox.settings.inited) return true 188 | else $.facebox.settings.inited = true 189 | 190 | $(document).trigger('init.facebox') 191 | makeCompatible() 192 | 193 | var imageTypes = $.facebox.settings.imageTypes.join('|') 194 | $.facebox.settings.imageTypesRegexp = new RegExp('\.(' + imageTypes + ')$', 'i') 195 | 196 | if (settings) $.extend($.facebox.settings, settings) 197 | $('body').append($.facebox.settings.faceboxHtml) 198 | 199 | var preload = [ new Image(), new Image() ] 200 | preload[0].src = $.facebox.settings.closeImage 201 | preload[1].src = $.facebox.settings.loadingImage 202 | 203 | $('#facebox').find('.b:first, .bl, .br, .tl, .tr').each(function() { 204 | preload.push(new Image()) 205 | preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1') 206 | }) 207 | 208 | $('#facebox .close').click($.facebox.close) 209 | $('#facebox .close_image').attr('src', $.facebox.settings.closeImage) 210 | } 211 | 212 | // getPageScroll() by quirksmode.com 213 | function getPageScroll() { 214 | var xScroll, yScroll; 215 | if (self.pageYOffset) { 216 | yScroll = self.pageYOffset; 217 | xScroll = self.pageXOffset; 218 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 219 | yScroll = document.documentElement.scrollTop; 220 | xScroll = document.documentElement.scrollLeft; 221 | } else if (document.body) {// all other Explorers 222 | yScroll = document.body.scrollTop; 223 | xScroll = document.body.scrollLeft; 224 | } 225 | return new Array(xScroll,yScroll) 226 | } 227 | 228 | // Adapted from getPageSize() by quirksmode.com 229 | function getPageHeight() { 230 | var windowHeight 231 | if (self.innerHeight) { // all except Explorer 232 | windowHeight = self.innerHeight; 233 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 234 | windowHeight = document.documentElement.clientHeight; 235 | } else if (document.body) { // other Explorers 236 | windowHeight = document.body.clientHeight; 237 | } 238 | return windowHeight 239 | } 240 | 241 | // Backwards compatibility 242 | function makeCompatible() { 243 | var $s = $.facebox.settings 244 | 245 | $s.loadingImage = $s.loading_image || $s.loadingImage 246 | $s.closeImage = $s.close_image || $s.closeImage 247 | $s.imageTypes = $s.image_types || $s.imageTypes 248 | $s.faceboxHtml = $s.facebox_html || $s.faceboxHtml 249 | } 250 | 251 | // Figures out what you want to display and displays it 252 | // formats are: 253 | // div: #id 254 | // image: blah.extension 255 | // ajax: anything else 256 | function fillFaceboxFromHref(href, klass) { 257 | // div 258 | if (href.match(/#/)) { 259 | var url = window.location.href.split('#')[0] 260 | var target = href.replace(url,'') 261 | if (target == '#') return 262 | $.facebox.reveal($(target).html(), klass) 263 | 264 | // image 265 | } else if (href.match($.facebox.settings.imageTypesRegexp)) { 266 | fillFaceboxFromImage(href, klass) 267 | // ajax 268 | } else { 269 | fillFaceboxFromAjax(href, klass) 270 | } 271 | } 272 | 273 | function fillFaceboxFromImage(href, klass) { 274 | var image = new Image() 275 | image.onload = function() { 276 | $.facebox.reveal('
', klass) 277 | } 278 | image.src = href 279 | } 280 | 281 | function fillFaceboxFromAjax(href, klass) { 282 | $.get(href, function(data) { $.facebox.reveal(data, klass) }) 283 | } 284 | 285 | function skipOverlay() { 286 | return $.facebox.settings.overlay == false || $.facebox.settings.opacity === null 287 | } 288 | 289 | function showOverlay() { 290 | if (skipOverlay()) return 291 | 292 | if ($('#facebox_overlay').length == 0) 293 | $("body").append('
') 294 | 295 | $('#facebox_overlay').hide().addClass("facebox_overlayBG") 296 | .css('opacity', $.facebox.settings.opacity) 297 | .click(function() { $(document).trigger('close.facebox') }) 298 | .fadeIn(200) 299 | return false 300 | } 301 | 302 | function hideOverlay() { 303 | if (skipOverlay()) return 304 | 305 | $('#facebox_overlay').fadeOut(200, function(){ 306 | $("#facebox_overlay").removeClass("facebox_overlayBG") 307 | $("#facebox_overlay").addClass("facebox_hide") 308 | $("#facebox_overlay").remove() 309 | }) 310 | 311 | return false 312 | } 313 | 314 | /* 315 | * Bindings 316 | */ 317 | 318 | $(document).bind('close.facebox', function() { 319 | $(document).unbind('keydown.facebox') 320 | $('#facebox').fadeOut(function() { 321 | $('#facebox .content').removeClass().addClass('content') 322 | hideOverlay() 323 | $('#facebox .loading').remove() 324 | }) 325 | }) 326 | 327 | })(jQuery); -------------------------------------------------------------------------------- /public/js/hurl.headers.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | $.fn.hurlHeaders = function(el) { 3 | $(el).autocompleteArray(keyNames( Headers ), { 4 | delay: 40, 5 | onItemSelect: function(e) { 6 | var header = $(e).text(), next = $(el).siblings('input') 7 | var more = Headers[header] 8 | 9 | if ( header == "User-Agent" ) { 10 | next.autocompleteArray(keyNames( Headers['User-Agent'] ), { 11 | delay: 40, 12 | onItemSelect: function(row) { 13 | next.val( Headers['User-Agent'][$(row).text()] ) 14 | } 15 | }) 16 | } else if ( more == "date" ) { 17 | next.focus().val( GetRFC822Date(new Date) ) 18 | } else if ( more ) { 19 | next.autocompleteArray( more, { delay: 40 } ) 20 | } 21 | 22 | next.focus() 23 | } 24 | }) 25 | } 26 | 27 | function keyNames(obj) { 28 | var names = [] 29 | for (name in obj) { 30 | names.push(name) 31 | } 32 | return names 33 | } 34 | 35 | var Headers = { 36 | "Accept": ["*/*", "text/plain", "text/html, text/plain", "application/xml", "application/json"], 37 | "Accept-Encoding": [ "compress", "deflate", "gzip", "compress, gzip", "gzip, deflate"], 38 | "Accept-Language": [ "en", "es", "de", "fr", "*" ], 39 | "Cache-Control": [ "cache", "no-cache" ], 40 | "Connection": [ "close", "keep-alive" ], 41 | "Cookie": null, 42 | "Content-Length": null, 43 | "Content-Type": [ "application/octet-stream", "application/x-www-form-urlencoded", "application/xml", "application/json", "text/html", "text/plain", "text/xml" ], 44 | "From": null, 45 | "Host": null, 46 | "If-Match": [ "*" ], 47 | "If-Modified-Since": "date", 48 | "If-None-Match": [ "*" ], 49 | "If-Range": "date", 50 | "If-Unmodified-Since": "date", 51 | "Max-Forwards": null, 52 | "Pragma": [ "cache", "no-cache" ], 53 | "User-Agent": { 54 | "Firefox 1.5.0.12 - Mac": "Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12", 55 | "Firefox 1.5.0.12 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12", 56 | "Firefox 2.0.0.12 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12", 57 | "Firefox 2.0.0.12 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12", 58 | "Firefox 3.0.4 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4", 59 | "Firefox 3.0.4 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/2008102920 Firefox/3.0.4", 60 | "Firefox 3.5.2 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2", 61 | "Firefox 3.5.2 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2", 62 | "Internet Explorer 5.2.3 – Mac": "Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)", 63 | "Internet Explorer 5.5": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1)", 64 | "Internet Explorer 6.0": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", 65 | "Internet Explorer 7.0": "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)", 66 | "Internet Explorer 8.0": "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0)", 67 | "Lynx 2.8.4rel.1 on Linux": "Lynx/2.8.4rel.1 libwww-FM/2.14", 68 | "MobileSafari 1.1.3 - iPhone": "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A93 Safari/419.3", 69 | "MobileSafari 1.1.3 - iPod touch": "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A93 Safari/419.3", 70 | "Opera 9.25 - Mac": "Opera/9.25 (Macintosh; Intel Mac OS X; U; en)", 71 | "Opera 9.25 - Windows": "Opera/9.25 (Windows NT 5.1; U; en)", 72 | "Safari 1.2.4 - Mac": "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12", 73 | "Safari 1.3.2 - Mac": "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6", 74 | "Safari 2.0.4 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", 75 | "Safari 3.0.4 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10", 76 | "Safari 3.1.2 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-us) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13", 77 | "Safari 3.1.2 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-us) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13", 78 | "Safari 3.2.1 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1", 79 | "Safari 3.2.1 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1", 80 | "Safari 4.0.2 - Mac": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19", 81 | "Safari 4.0.2 - Windows": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1" 82 | } 83 | } 84 | 85 | 86 | /* 87 | * Stolen without mercy nor remorse from 88 | * http://www.sanctumvoid.net/jsexamples/rfc822datetime/rfc822datetime.html 89 | */ 90 | 91 | /*Accepts a Javascript Date object as the parameter; 92 | outputs an RFC822-formatted datetime string. */ 93 | function GetRFC822Date(oDate) 94 | { 95 | var aMonths = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", 96 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"); 97 | 98 | var aDays = new Array( "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); 99 | var dtm = new String(); 100 | 101 | dtm = aDays[oDate.getDay()] + ", "; 102 | dtm += padWithZero(oDate.getDate()) + " "; 103 | dtm += aMonths[oDate.getMonth()] + " "; 104 | dtm += oDate.getFullYear() + " "; 105 | dtm += padWithZero(oDate.getHours()) + ":"; 106 | dtm += padWithZero(oDate.getMinutes()) + ":"; 107 | dtm += padWithZero(oDate.getSeconds()) + " " ; 108 | dtm += getTZOString(oDate.getTimezoneOffset()); 109 | return dtm; 110 | } 111 | 112 | //Pads numbers with a preceding 0 if the number is less than 10. 113 | function padWithZero(val) 114 | { 115 | if (parseInt(val) < 10) 116 | { 117 | return "0" + val; 118 | } 119 | return val; 120 | } 121 | 122 | /* accepts the client's time zone offset from GMT in minutes as a parameter. 123 | returns the timezone offset in the format [+|-}DDDD */ 124 | function getTZOString(timezoneOffset) 125 | { 126 | var hours = Math.floor(timezoneOffset/60); 127 | var modMin = Math.abs(timezoneOffset%60); 128 | var s = new String(); 129 | s += (hours > 0) ? "-" : "+"; 130 | var absHours = Math.abs(hours) 131 | s += (absHours < 10) ? "0" + absHours :absHours; 132 | s += ((modMin == 0) ? "00" : modMin); 133 | return(s); 134 | } 135 | })(); -------------------------------------------------------------------------------- /public/js/hurl.js: -------------------------------------------------------------------------------- 1 | var Hurl = { 2 | // apply label hints to inputs based on their 3 | // title attribute 4 | labelHints: function(el) { 5 | $(el).each(function() { 6 | var self = $(this), title = self.attr('title') 7 | 8 | // indicate inputs using defaults 9 | self.addClass('defaulted') 10 | 11 | if (self.val() === '' || self.val() === title) { 12 | self.val(title).css('color', '#E9EAEA') 13 | } else { 14 | self.addClass('focused') 15 | } 16 | 17 | self.focus(function() { 18 | if (self.val() === title) { 19 | self.val('').addClass('focused').css('color', '#333') 20 | } 21 | }) 22 | 23 | self.blur(function() { 24 | if (self.val() === '') { 25 | self.val(title).removeClass('focused').css('color', '#E9EAEA') 26 | } 27 | }) 28 | }) 29 | }, 30 | 31 | removeEmptyData: function(data) { 32 | var keepers = [], value 33 | 34 | // remove empty arrays and any default titular data 35 | for (key in data) { 36 | if (value = data[key].value) { 37 | if ($('input[name=' + data[key].name +'].defaulted:not(.focused)').val() != value) { 38 | keepers.push(data[key]) 39 | } 40 | } 41 | } 42 | 43 | data.splice(0, data.length) 44 | 45 | for (key in keepers) 46 | data.push( keepers[key] ) 47 | 48 | return true 49 | }, 50 | 51 | pony: function() { 52 | if (!this.ponyLoaded) return this.loadPony() 53 | if (this.ponying) return 54 | this.ponying = true 55 | 56 | var width = 668 57 | 58 | var pony = $("
").css({ 59 | width: width, 60 | height: 422, 61 | background: 'url(/img/pony.png) top center', 62 | position: 'fixed', 63 | bottom: 0, 64 | right: 0-width, 65 | "z-index": 1000, 66 | cursor: 'pointer' 67 | }).appendTo($("body")) 68 | 69 | pony.show().animate({right: 0}, 1500, function() { 70 | setTimeout(function() { 71 | pony.css('background', 'url(/img/pony-hurl.png) top center') 72 | setTimeout(function() { 73 | pony.animate({right: 0-width}, 1500, function() { 74 | Hurl.ponying = false 75 | }) 76 | }, 500) 77 | }, 1000) 78 | }) 79 | }, 80 | 81 | loadPony: function() { 82 | $(new Image()).load(function() { 83 | Hurl.loadOtherPony() 84 | }).attr('src', '/img/pony.png'); 85 | }, 86 | 87 | loadOtherPony: function() { 88 | $(new Image()).load(function() { 89 | Hurl.ponyLoaded = true 90 | Hurl.pony() 91 | }).attr('src', '/img/pony-hurl.png'); 92 | } 93 | } 94 | 95 | $.fn.hurlAjaxSubmit = function(callback) { 96 | return $(this).ajaxSubmit({ 97 | beforeSubmit: Hurl.removeEmptyData, 98 | success: callback 99 | }) 100 | } 101 | 102 | $(document).ready(function() { 103 | // select method 104 | $('#select-method').change(function() { 105 | $('#select-method option:selected').each(function() { 106 | var method = $(this).attr('value') 107 | if (method == 'POST' || method == 'PUT'){ 108 | $('#post-params').show() 109 | } else { 110 | $('#post-params').hide() 111 | } 112 | }) 113 | }) 114 | $('#select-method').change() 115 | 116 | // add auth 117 | $('input[name=auth]').change(function() { 118 | if ($(this).attr('value') == 'basic') { 119 | $('#basic-auth-fields').show() 120 | $('#basic-auth-fields .form-alpha').focus() 121 | } else { 122 | $('#basic-auth-fields').hide() 123 | } 124 | }) 125 | $('#auth-selection :checked').change() 126 | 127 | // add post param 128 | $('#add-param').click(function() { 129 | // toggle if post body is being shown 130 | if ( $('#set-post-body .link-icon').text() == '-' ) { 131 | $('#set-post-body').click() 132 | return false 133 | } 134 | 135 | var newField = $('#param-fields').clone() 136 | newField.toggle().attr('id', '') 137 | newField.addClass('param-field') 138 | newField.find('.form-alpha').attr('title', 'name') 139 | newField.find('.form-beta').attr('title', 'value') 140 | Hurl.labelHints( newField.find('input[title]') ) 141 | registerRemoveHandlers( newField, '.param-delete' ) 142 | $(this).parent().append( newField ) 143 | return false 144 | }) 145 | 146 | // set post body 147 | $('#set-post-body').click(function() { 148 | var icon = $(this).find('.link-icon') 149 | 150 | if ( icon.text() == '+' ) { 151 | icon.text('-') 152 | $('.param-field').hide() 153 | $('#post-body').show().find('textarea').attr('disabled', false) 154 | } else { 155 | icon.text('+') 156 | $('.param-field').show() 157 | $('#post-body').hide().find('textarea').attr('disabled', true) 158 | } 159 | 160 | return false 161 | }) 162 | 163 | if ( $('#post-body').is(':visible') ) { 164 | $('#set-post-body').click() 165 | } 166 | 167 | // add header 168 | $('#add-header').click(function() { 169 | var newField = $('#header-fields').clone() 170 | newField.toggle().attr('id', '') 171 | newField.find('.form-alpha').hurlHeaders() 172 | newField.find('.form-alpha').attr('title', 'name') 173 | newField.find('.form-beta').attr('title', 'value') 174 | Hurl.labelHints( newField.find('input[title]') ) 175 | registerRemoveHandlers( newField, '.header-delete' ) 176 | $(this).parent().append( newField ) 177 | return false 178 | }) 179 | 180 | // remove header / param 181 | function registerRemoveHandlers(el, klass) { 182 | $(el).find(klass).click(function() { 183 | $(this).parents('p:first').remove() 184 | return false 185 | }) 186 | } 187 | 188 | registerRemoveHandlers( document, '.header-delete' ) 189 | registerRemoveHandlers( document, '.param-delete' ) 190 | 191 | // hurl it! 192 | $('#hurl-form').submit(function() { 193 | $('#send-wrap').children().toggle() 194 | $('.flash-error, .flash-notice').fadeOut() 195 | $('#request-and-response').hide() 196 | 197 | $(this).hurlAjaxSubmit(function(res) { 198 | var data = JSON.parse(res) 199 | 200 | if (data.error) { 201 | $('#flash-error-msg').html(data.error) 202 | $('.flash-error').show() 203 | } else if (/hurl/.test(location.pathname) && data.hurl_id && data.view_id) { 204 | window.location = '/hurls/' + data.hurl_id + '/' + data.view_id 205 | } else if (data.header && data.body && data.request) { 206 | if ( /railsrumble/.test($('input[name=url]').val()) ) Hurl.pony() 207 | if (data.prev_hurl) { 208 | $('#page-prev').attr('href', '/hurls/' + data.prev_hurl).show() 209 | $('#page-next').attr('href', '/').show() 210 | } 211 | $('.permalink').attr('href', '/hurls/'+data.hurl_id+'/'+data.view_id) 212 | $('.full-size-link').attr('href', '/views/' + data.view_id) 213 | $('#request').html(data.request) 214 | $('#response').html('
' + data.header + '
' + data.body) 215 | $('.help-blurb').hide() 216 | $('#request-and-response').show() 217 | } else { 218 | $('#flash-error-msg').html("Weird response. Sorry.") 219 | $('.flash-error').show() 220 | } 221 | 222 | $('#send-wrap').children().toggle() 223 | }) 224 | 225 | return false 226 | }) 227 | 228 | // delete hurl 229 | $('.hurl-delete').click(function() { 230 | $(this).parents('tr:first').remove() 231 | $.ajax({type: 'DELETE', url: $(this).attr('href')}) 232 | return false 233 | }) 234 | 235 | // toggle request/response display 236 | $('.toggle-reqres-link').click(function(){ 237 | $('.toggle-reqres').toggle() 238 | $('#code-request').toggle() 239 | $('#code-response').toggle() 240 | return false 241 | }) 242 | 243 | // flash close 244 | $('.flash-close').click(function (){ 245 | $(this).parent().fadeOut() 246 | return false 247 | }) 248 | 249 | // facebox 250 | $('a[rel*=facebox]').facebox({ opacity: 0.4 }) 251 | $(document).bind('reveal.facebox', function() { 252 | Hurl.labelHints('#facebox input[title]') 253 | $('#facebox .footer').remove() 254 | }) 255 | 256 | // in-field labels 257 | Hurl.labelHints('input[title]') 258 | 259 | // relatize dates 260 | $('.relatize').relatizeDate() 261 | }); 262 | -------------------------------------------------------------------------------- /public/js/jquery.autocomplete.js: -------------------------------------------------------------------------------- 1 | jQuery.autocomplete = function(input, options) { 2 | // Create a link to self 3 | var me = this; 4 | 5 | // Create jQuery object for input element 6 | var $input = $(input).attr("autocomplete", "off"); 7 | 8 | // Apply inputClass if necessary 9 | if (options.inputClass) $input.addClass(options.inputClass); 10 | 11 | // Create results 12 | var results = document.createElement("div"); 13 | // Create jQuery object for results 14 | var $results = $(results); 15 | $results.hide().addClass(options.resultsClass).css("position", "absolute"); 16 | if( options.width > 0 ) $results.css("width", options.width); 17 | 18 | // Add to body element 19 | $("body").append(results); 20 | 21 | input.autocompleter = me; 22 | 23 | var timeout = null; 24 | var prev = ""; 25 | var active = -1; 26 | var cache = {}; 27 | var keyb = false; 28 | var hasFocus = false; 29 | var lastKeyPressCode = null; 30 | 31 | // flush cache 32 | function flushCache(){ 33 | cache = {}; 34 | cache.data = {}; 35 | cache.length = 0; 36 | }; 37 | 38 | // flush cache 39 | flushCache(); 40 | 41 | // if there is a data array supplied 42 | if( options.data != null ){ 43 | var sFirstChar = "", stMatchSets = {}, row = []; 44 | 45 | // no url was specified, we need to adjust the cache length to make sure it fits the local data store 46 | if( typeof options.url != "string" ) options.cacheLength = 1; 47 | 48 | // loop through the array and create a lookup structure 49 | for( var i=0; i < options.data.length; i++ ){ 50 | // if row is a string, make an array otherwise just reference the array 51 | row = ((typeof options.data[i] == "string") ? [options.data[i]] : options.data[i]); 52 | 53 | // if the length is zero, don't add to list 54 | if( row[0].length > 0 ){ 55 | // get the first character 56 | sFirstChar = row[0].substring(0, 1).toLowerCase(); 57 | // if no lookup array for this character exists, look it up now 58 | if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = []; 59 | // if the match is a string 60 | stMatchSets[sFirstChar].push(row); 61 | } 62 | } 63 | 64 | // add the data items to the cache 65 | for( var k in stMatchSets ){ 66 | // increase the cache size 67 | options.cacheLength++; 68 | // add to the cache 69 | addToCache(k, stMatchSets[k]); 70 | } 71 | } 72 | 73 | $input 74 | .keydown(function(e) { 75 | // track last key pressed 76 | lastKeyPressCode = e.keyCode; 77 | switch(e.keyCode) { 78 | case 38: // up 79 | e.preventDefault(); 80 | moveSelect(-1); 81 | break; 82 | case 40: // down 83 | e.preventDefault(); 84 | moveSelect(1); 85 | break; 86 | case 9: // tab 87 | case 13: // return 88 | if( selectCurrent() ){ 89 | // make sure to blur off the current field 90 | $input.get(0).blur(); 91 | e.preventDefault(); 92 | } 93 | break; 94 | default: 95 | active = -1; 96 | if (timeout) clearTimeout(timeout); 97 | timeout = setTimeout(function(){onChange();}, options.delay); 98 | break; 99 | } 100 | }) 101 | .focus(function(){ 102 | // track whether the field has focus, we shouldn't process any results if the field no longer has focus 103 | hasFocus = true; 104 | }) 105 | .blur(function() { 106 | // track whether the field has focus 107 | hasFocus = false; 108 | hideResults(); 109 | }); 110 | 111 | hideResultsNow(); 112 | 113 | function onChange() { 114 | // ignore if the following keys are pressed: [del] [shift] [capslock] 115 | if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ) return $results.hide(); 116 | var v = $input.val(); 117 | if (v == prev) return; 118 | prev = v; 119 | if (v.length >= options.minChars) { 120 | $input.addClass(options.loadingClass); 121 | requestData(v); 122 | } else { 123 | $input.removeClass(options.loadingClass); 124 | $results.hide(); 125 | } 126 | }; 127 | 128 | function moveSelect(step) { 129 | 130 | var lis = $("li", results); 131 | if (!lis) return; 132 | 133 | active += step; 134 | 135 | if (active < 0) { 136 | active = 0; 137 | } else if (active >= lis.size()) { 138 | active = lis.size() - 1; 139 | } 140 | 141 | lis.removeClass("ac_over"); 142 | 143 | $(lis[active]).addClass("ac_over"); 144 | 145 | // Weird behaviour in IE 146 | // if (lis[active] && lis[active].scrollIntoView) { 147 | // lis[active].scrollIntoView(false); 148 | // } 149 | 150 | }; 151 | 152 | function selectCurrent() { 153 | var li = $("li.ac_over", results)[0]; 154 | if (!li) { 155 | var $li = $("li", results); 156 | if (options.selectOnly) { 157 | if ($li.length == 1) li = $li[0]; 158 | } else if (options.selectFirst) { 159 | li = $li[0]; 160 | } 161 | } 162 | if (li) { 163 | selectItem(li); 164 | return true; 165 | } else { 166 | return false; 167 | } 168 | }; 169 | 170 | function selectItem(li) { 171 | if (!li) { 172 | li = document.createElement("li"); 173 | li.extra = []; 174 | li.selectValue = ""; 175 | } 176 | var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML); 177 | input.lastSelected = v; 178 | prev = v; 179 | $results.html(""); 180 | $input.val(v); 181 | hideResultsNow(); 182 | if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1); 183 | }; 184 | 185 | // selects a portion of the input string 186 | function createSelection(start, end){ 187 | // get a reference to the input element 188 | var field = $input.get(0); 189 | if( field.createTextRange ){ 190 | var selRange = field.createTextRange(); 191 | selRange.collapse(true); 192 | selRange.moveStart("character", start); 193 | selRange.moveEnd("character", end); 194 | selRange.select(); 195 | } else if( field.setSelectionRange ){ 196 | field.setSelectionRange(start, end); 197 | } else { 198 | if( field.selectionStart ){ 199 | field.selectionStart = start; 200 | field.selectionEnd = end; 201 | } 202 | } 203 | field.focus(); 204 | }; 205 | 206 | // fills in the input box w/the first match (assumed to be the best match) 207 | function autoFill(sValue){ 208 | // if the last user key pressed was backspace, don't autofill 209 | if( lastKeyPressCode != 8 ){ 210 | // fill in the value (keep the case the user has typed) 211 | $input.val($input.val() + sValue.substring(prev.length)); 212 | // select the portion of the value not typed by the user (so the next character will erase) 213 | createSelection(prev.length, sValue.length); 214 | } 215 | }; 216 | 217 | function showResults() { 218 | // get the position of the input field right now (in case the DOM is shifted) 219 | var pos = findPos(input); 220 | // either use the specified width, or autocalculate based on form element 221 | var iWidth = (options.width > 0) ? options.width : $input.width(); 222 | // reposition 223 | $results.css({ 224 | width: parseInt(iWidth) + "px", 225 | top: (pos.y + input.offsetHeight) + "px", 226 | left: pos.x + "px" 227 | }).show(); 228 | }; 229 | 230 | function hideResults() { 231 | if (timeout) clearTimeout(timeout); 232 | timeout = setTimeout(hideResultsNow, 200); 233 | }; 234 | 235 | function hideResultsNow() { 236 | if (timeout) clearTimeout(timeout); 237 | $input.removeClass(options.loadingClass); 238 | if ($results.is(":visible")) { 239 | $results.hide(); 240 | } 241 | if (options.mustMatch) { 242 | var v = $input.val(); 243 | if (v != input.lastSelected) { 244 | selectItem(null); 245 | } 246 | } 247 | }; 248 | 249 | function receiveData(q, data) { 250 | if (data) { 251 | $input.removeClass(options.loadingClass); 252 | results.innerHTML = ""; 253 | 254 | // if the field no longer has focus or if there are no matches, do not display the drop down 255 | if( !hasFocus || data.length == 0 ) return hideResultsNow(); 256 | 257 | if ($.browser.msie) { 258 | // we put a styled iframe behind the calendar so HTML SELECT elements don't show through 259 | $results.append(document.createElement('iframe')); 260 | } 261 | results.appendChild(dataToDom(data)); 262 | // autofill in the complete box w/the first match as long as the user hasn't entered in more data 263 | if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]); 264 | showResults(); 265 | } else { 266 | hideResultsNow(); 267 | } 268 | }; 269 | 270 | function parseData(data) { 271 | if (!data) return null; 272 | var parsed = []; 273 | var rows = data.split(options.lineSeparator); 274 | for (var i=0; i < rows.length; i++) { 275 | var row = $.trim(rows[i]); 276 | if (row) { 277 | parsed[parsed.length] = row.split(options.cellSeparator); 278 | } 279 | } 280 | return parsed; 281 | }; 282 | 283 | function dataToDom(data) { 284 | var ul = document.createElement("ul"); 285 | var num = data.length; 286 | 287 | // limited results to a max number 288 | if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow; 289 | 290 | for (var i=0; i < num; i++) { 291 | var row = data[i]; 292 | if (!row) continue; 293 | var li = document.createElement("li"); 294 | if (options.formatItem) { 295 | li.innerHTML = options.formatItem(row, i, num); 296 | li.selectValue = row[0]; 297 | } else { 298 | li.innerHTML = row[0]; 299 | li.selectValue = row[0]; 300 | } 301 | var extra = null; 302 | if (row.length > 1) { 303 | extra = []; 304 | for (var j=1; j < row.length; j++) { 305 | extra[extra.length] = row[j]; 306 | } 307 | } 308 | li.extra = extra; 309 | ul.appendChild(li); 310 | $(li).hover( 311 | function() { $("li", ul).removeClass("ac_over"); $(this).addClass("ac_over"); active = $("li", ul).indexOf($(this).get(0)); }, 312 | function() { $(this).removeClass("ac_over"); } 313 | ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) }); 314 | } 315 | return ul; 316 | }; 317 | 318 | function requestData(q) { 319 | if (!options.matchCase) q = q.toLowerCase(); 320 | var data = options.cacheLength ? loadFromCache(q) : null; 321 | // recieve the cached data 322 | if (data) { 323 | receiveData(q, data); 324 | // if an AJAX url has been supplied, try loading the data now 325 | } else if( (typeof options.url == "string") && (options.url.length > 0) ){ 326 | $.get(makeUrl(q), function(data) { 327 | data = parseData(data); 328 | addToCache(q, data); 329 | receiveData(q, data); 330 | }); 331 | // if there's been no data found, remove the loading class 332 | } else { 333 | $input.removeClass(options.loadingClass); 334 | } 335 | }; 336 | 337 | function makeUrl(q) { 338 | var url = options.url + "?q=" + encodeURI(q); 339 | for (var i in options.extraParams) { 340 | url += "&" + i + "=" + encodeURI(options.extraParams[i]); 341 | } 342 | return url; 343 | }; 344 | 345 | function loadFromCache(q) { 346 | if (!q) return null; 347 | if (cache.data[q]) return cache.data[q]; 348 | if (options.matchSubset) { 349 | for (var i = q.length - 1; i >= options.minChars; i--) { 350 | var qs = q.substr(0, i); 351 | var c = cache.data[qs]; 352 | if (c) { 353 | var csub = []; 354 | for (var j = 0; j < c.length; j++) { 355 | var x = c[j]; 356 | var x0 = x[0]; 357 | if (matchSubset(x0, q)) { 358 | csub[csub.length] = x; 359 | } 360 | } 361 | return csub; 362 | } 363 | } 364 | } 365 | return null; 366 | }; 367 | 368 | function matchSubset(s, sub) { 369 | if (!options.matchCase) s = s.toLowerCase(); 370 | var i = s.indexOf(sub); 371 | if (i == -1) return false; 372 | return i == 0 || options.matchContains; 373 | }; 374 | 375 | this.flushCache = function() { 376 | flushCache(); 377 | }; 378 | 379 | this.setExtraParams = function(p) { 380 | options.extraParams = p; 381 | }; 382 | 383 | this.findValue = function(){ 384 | var q = $input.val(); 385 | 386 | if (!options.matchCase) q = q.toLowerCase(); 387 | var data = options.cacheLength ? loadFromCache(q) : null; 388 | if (data) { 389 | findValueCallback(q, data); 390 | } else if( (typeof options.url == "string") && (options.url.length > 0) ){ 391 | $.get(makeUrl(q), function(data) { 392 | data = parseData(data) 393 | addToCache(q, data); 394 | findValueCallback(q, data); 395 | }); 396 | } else { 397 | // no matches 398 | findValueCallback(q, null); 399 | } 400 | } 401 | 402 | function findValueCallback(q, data){ 403 | if (data) $input.removeClass(options.loadingClass); 404 | 405 | var num = (data) ? data.length : 0; 406 | var li = null; 407 | 408 | for (var i=0; i < num; i++) { 409 | var row = data[i]; 410 | 411 | if( row[0].toLowerCase() == q.toLowerCase() ){ 412 | li = document.createElement("li"); 413 | if (options.formatItem) { 414 | li.innerHTML = options.formatItem(row, i, num); 415 | li.selectValue = row[0]; 416 | } else { 417 | li.innerHTML = row[0]; 418 | li.selectValue = row[0]; 419 | } 420 | var extra = null; 421 | if( row.length > 1 ){ 422 | extra = []; 423 | for (var j=1; j < row.length; j++) { 424 | extra[extra.length] = row[j]; 425 | } 426 | } 427 | li.extra = extra; 428 | } 429 | } 430 | 431 | if( options.onFindValue ) setTimeout(function() { options.onFindValue(li) }, 1); 432 | } 433 | 434 | function addToCache(q, data) { 435 | if (!data || !q || !options.cacheLength) return; 436 | if (!cache.length || cache.length > options.cacheLength) { 437 | flushCache(); 438 | cache.length++; 439 | } else if (!cache[q]) { 440 | cache.length++; 441 | } 442 | cache.data[q] = data; 443 | }; 444 | 445 | function findPos(obj) { 446 | var curleft = obj.offsetLeft || 0; 447 | var curtop = obj.offsetTop || 0; 448 | while (obj = obj.offsetParent) { 449 | curleft += obj.offsetLeft 450 | curtop += obj.offsetTop 451 | } 452 | return {x:curleft,y:curtop}; 453 | } 454 | } 455 | 456 | jQuery.fn.autocomplete = function(url, options, data) { 457 | // Make sure options exists 458 | options = options || {}; 459 | // Set url as option 460 | options.url = url; 461 | // set some bulk local data 462 | options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null; 463 | 464 | // Set default values for required options 465 | options.inputClass = options.inputClass || "ac_input"; 466 | options.resultsClass = options.resultsClass || "ac_results"; 467 | options.lineSeparator = options.lineSeparator || "\n"; 468 | options.cellSeparator = options.cellSeparator || "|"; 469 | options.minChars = options.minChars || 1; 470 | options.delay = options.delay || 400; 471 | options.matchCase = options.matchCase || 0; 472 | options.matchSubset = options.matchSubset || 1; 473 | options.matchContains = options.matchContains || 0; 474 | options.cacheLength = options.cacheLength || 1; 475 | options.mustMatch = options.mustMatch || 0; 476 | options.extraParams = options.extraParams || {}; 477 | options.loadingClass = options.loadingClass || "ac_loading"; 478 | options.selectFirst = options.selectFirst || false; 479 | options.selectOnly = options.selectOnly || false; 480 | options.maxItemsToShow = options.maxItemsToShow || -1; 481 | options.autoFill = options.autoFill || false; 482 | options.width = parseInt(options.width, 10) || 0; 483 | 484 | this.each(function() { 485 | var input = this; 486 | new jQuery.autocomplete(input, options); 487 | }); 488 | 489 | // Don't break the chain 490 | return this; 491 | } 492 | 493 | jQuery.fn.autocompleteArray = function(data, options) { 494 | return this.autocomplete(null, options, data); 495 | } 496 | 497 | jQuery.fn.indexOf = function(e){ 498 | for( var i=0; i= 0 ? '&' : '?') + q; 113 | options.data = null; // data is null for 'get' 114 | } 115 | else 116 | options.data = q; // data is the query string for 'post' 117 | 118 | var $form = this, callbacks = []; 119 | if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); 120 | if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); 121 | 122 | // perform a load on the target only if dataType is not provided 123 | if (!options.dataType && options.target) { 124 | var oldSuccess = options.success || function(){}; 125 | callbacks.push(function(data) { 126 | $(options.target).html(data).each(oldSuccess, arguments); 127 | }); 128 | } 129 | else if (options.success) 130 | callbacks.push(options.success); 131 | 132 | options.success = function(data, status) { 133 | for (var i=0, max=callbacks.length; i < max; i++) 134 | callbacks[i].apply(options, [data, status, $form]); 135 | }; 136 | 137 | // are there files to upload? 138 | var files = $('input:file', this).fieldValue(); 139 | var found = false; 140 | for (var j=0; j < files.length; j++) 141 | if (files[j]) 142 | found = true; 143 | 144 | var multipart = false; 145 | // var mp = 'multipart/form-data'; 146 | // multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); 147 | 148 | // options.iframe allows user to force iframe mode 149 | if (options.iframe || found || multipart) { 150 | // hack to fix Safari hang (thanks to Tim Molendijk for this) 151 | // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d 152 | if (options.closeKeepAlive) 153 | $.get(options.closeKeepAlive, fileUpload); 154 | else 155 | fileUpload(); 156 | } 157 | else 158 | $.ajax(options); 159 | 160 | // fire 'notify' event 161 | this.trigger('form-submit-notify', [this, options]); 162 | return this; 163 | 164 | 165 | // private function for handling file uploads (hat tip to YAHOO!) 166 | function fileUpload() { 167 | var form = $form[0]; 168 | 169 | if ($(':input[name=submit]', form).length) { 170 | alert('Error: Form elements must not be named "submit".'); 171 | return; 172 | } 173 | 174 | var opts = $.extend({}, $.ajaxSettings, options); 175 | var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); 176 | 177 | var id = 'jqFormIO' + (new Date().getTime()); 178 | var $io = $('