public
Created

Command line utility for publishing documents on hashify.me (and docco.hashify.me!)

  • Download Gist
hashify
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
#!/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]

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.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.