Skip to content

Instantly share code, notes, and snippets.

@davidchambers
Created October 13, 2011 06:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidchambers/1283564 to your computer and use it in GitHub Desktop.
Save davidchambers/1283564 to your computer and use it in GitHub Desktop.
Command line utility for publishing documents on hashify.me (and docco.hashify.me!)
#!/usr/bin/env ruby
# Publish documents on the internets!
# -----------------------------------
# Copyright (c) 2011, David Chambers.
# * * * * * * * * * * * * * * * * * *
require 'base64'
require 'net/http'
require 'pathname'
require 'json' # `gem install json` if necessary
# bitly API credentials.
# It's fine to use these values (Hashify itself does).
$bitly_username = 'davidchambers'
$bitly_api_key = 'R_20d23528ed6381ebb614a997de11c20a'
# Variant display modes.
modes = ['presentation']
# Map synonyms to canonical symbols.
symbols = {
:docco => ['-d', '--docco'],
:help => ['-h', '--help'],
:mode => ['-m', '--mode'],
:open => ['-o', '--open'],
:prettify => ['-p', '--prettify'],
:raw => ['-r', '--raw'],
}
# Generate a hash with all the valid flags as its keys.
options = {}
symbols.each do |symbol, flags|
flags.each {|flag| options[flag] = symbol}
end
args = {}
path = previous = nil
ARGV.each do |arg|
# Argument is a valid flag.
if options.include? arg
previous = options[arg]
args[previous] = true
else
# Argument is a qualifier for the preceding flag.
if symbols.include? previous
args[previous] = arg == 'yes' ? true : arg == 'no' ? false : arg
# Argument is neither a valid flag nor preceded by a valid flag.
else
path = arg
end
previous = nil
end
end
if args[:help]
puts '''
usage: hashify path/to/file [options]
options:
-d, --docco use docco.hashify.me rather than hashify.me
-m, --mode MODE editor is hidden in "presentation" mode
-o, --open open document in default web browser
-p, --prettify [yes|no] google-code-prettify is invoked unless "no"
-r, --raw generate URL for text/plain version
-h, --help show this overview
'''.gsub /^[ ]{4}|\s+\Z/, ''
exit
end
raise ArgumentError, 'no file specified' if not path
# Return the Base64-encoded equivalent of `text`, *without* the line
# breaks Ruby so helpfully inserts.
def encode text
Base64.encode64(text).gsub /\n/, ''
end
# Send a "shorten" request to bitly, and return a data hash if things go
# well (the hash should contain "hash" and "url" keys). A `RuntimeError`
# is raised if bitly returns a non-200 status code.
def shorten url
# Generate query string from parameters.
req = Net::HTTP::Get.new '/'
req.set_form_data(
:login => $bitly_username,
:apiKey => $bitly_api_key,
:longUrl => url,
)
# Send request to bitly.
req = Net::HTTP::Get.new "/v3/shorten?#{req.body}"
res = Net::HTTP.new('api.bitly.com').request req
res = JSON.parse res.body
unless (code = res['status_code']) == 200
raise RuntimeError, "bad response from bitly (#{code})"
end
res['data']
end
# Read the contents of the specified file to determine the body of the
# Hashify document. For Docco documents, include the file's name as the
# document's title (partly for more reliable syntax highlighting).
contents = IO.read path
contents = "#{Pathname.new(path).basename}\n\n#{contents}" if args[:docco]
params = {}
# Use presentation mode by default for Docco documents.
params[:mode] = :presentation if args[:docco]
# Ignore unqualified, invalid, or implicit `--mode`.
params[:mode] = args[:mode] if modes.include? args[:mode]
# Only include "prettify" parameter if `--prettify no`.
params[:prettify] = :no if not args[:docco] and args[:prettify] == false
# Hashify uses "k1:v1;k2:v2" rather than "k1=v1&k2=v2".
search = params.map {|param, value| "#{param}:#{value}"}.join ';'
search = "?#{search}" unless search.empty?
hostname = 'hashify.me'
hostname = 'docco.' + hostname if args[:docco]
# Generate a Hashify URL from the file's contents and provided options.
domain = "http://#{hostname}/"
url = domain + encode(contents) + search
max = 2048
if url.length <= max
url = shorten(url)['url']
else
chunks = []
max -= domain.length
# It's necessary to treat both `nil` and `""` as exit conditions.
# Virtually always, `idx` will be larger than `contents.length`:
#
# >> 'foo'[7..-1]
# => nil
#
# Should the two happen to be equal, though, the result is `""`:
#
# >> 'qux bar'[7..-1]
# => ""
until contents.nil? or contents.empty?
# Three characters of binary text generally produce four characters
# of ASCII text when Base64 encoded:
#
# >> encode 'foo'
# => "Zm9v"
#
# Characters outside a defined range cannot be represented in ASCII
# so succinctly:
#
# >> encode 'π„žπ„žπ„ž'
# => "8J2EnvCdhJ7wnYSe"
# >> 'π„žπ„žπ„ž'.length
# => 3
#
# First we take the longest slice that *may* produce a string of an
# acceptable length. We then Base64 encode the slice. If the length
# of the resulting string exceeds `max`, we decrement `idx` and try
# again. We continue until we've found the longest acceptable slice.
idx = (max * 3/4).floor
idx -= 1 while (chunk = encode contents[0...idx]).length > max
contents = contents[idx..-1] # remainder
chunks.push chunk
end
# bitly allows up to 15 short URLs to be expanded in a single request.
# Hashify thus accepts a maximum of 15 bitly hashes in "packed" URLs.
raise ArgumentError, 'file too big for bitly :(' if chunks.length > 15
hashes = chunks.map {|chunk| shorten(domain + chunk)['hash']}
url = shorten(domain + 'unpack:' + hashes.join(',') + search)['url']
end
# Print short URL to stdout.
puts url
# Open document in default web browser if requested.
`open #{url}` if args[:open]
@davidchambers
Copy link
Author

Example usage:

$ hashify /usr/bin/hashify --docco --open
http://bit.ly/qihBun

This script's annotated source will appear in one's default browser. Meta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment