murlsh 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/.htaccess +7 -0
- data/COPYING +674 -0
- data/README.textile +32 -0
- data/Rakefile +192 -0
- data/VERSION +1 -0
- data/bin/murlsh +16 -0
- data/config.ru +11 -0
- data/config.yaml +18 -0
- data/lib/murlsh/atom_feed.rb +76 -0
- data/lib/murlsh/auth.rb +33 -0
- data/lib/murlsh/dispatch.rb +54 -0
- data/lib/murlsh/get_content_type.rb +84 -0
- data/lib/murlsh/get_title.rb +65 -0
- data/lib/murlsh/markup.rb +97 -0
- data/lib/murlsh/plugin.rb +23 -0
- data/lib/murlsh/referrer.rb +47 -0
- data/lib/murlsh/sqlite3_adapter.rb +12 -0
- data/lib/murlsh/time.rb +18 -0
- data/lib/murlsh/url.rb +44 -0
- data/lib/murlsh/url_body.rb +155 -0
- data/lib/murlsh/url_server.rb +82 -0
- data/lib/murlsh/xhtml_response.rb +19 -0
- data/lib/murlsh.rb +16 -0
- data/murlsh.gemspec +109 -0
- data/plugins/update_feed.rb +23 -0
- data/public/css/jquery.jgrowl.css +127 -0
- data/public/css/phone.css +14 -0
- data/public/css/screen.css +94 -0
- data/public/js/jquery-1.3.2.min.js +19 -0
- data/public/js/jquery.cookie.js +96 -0
- data/public/js/jquery.jgrowl_compressed.js +100 -0
- data/public/js/js.js +229 -0
- data/public/swf/player_mp3_mini.swf +0 -0
- data/test/auth_test.rb +34 -0
- data/test/get_charset_test.rb +25 -0
- data/test/get_content_type_test.rb +63 -0
- data/test/get_title_test.rb +43 -0
- data/test/markup_test.rb +144 -0
- data/test/referrer_test.rb +71 -0
- data/test/xhtml_response_test.rb +139 -0
- metadata +170 -0
data/README.textile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Simple site for a small group of people to share or archive urls.
|
2
|
+
|
3
|
+
* looks up url titles
|
4
|
+
* adds thumbnails for and jGrowls embedded versions of Flickr, Imageshack, Vimeo and YouTube urls
|
5
|
+
* embeds Flash mp3 player for mp3 urls
|
6
|
+
* Gravatar support
|
7
|
+
* Atom feed
|
8
|
+
* regex search
|
9
|
+
* looks good on iPhone
|
10
|
+
* rack interface
|
11
|
+
* plug-in interface
|
12
|
+
|
13
|
+
See "http://urls.matthewm.boedicker.org/":http://urls.matthewm.boedicker.org/ for example.
|
14
|
+
|
15
|
+
Phusion Passenger Setup:
|
16
|
+
|
17
|
+
<pre>
|
18
|
+
<code>
|
19
|
+
rake gemspec build
|
20
|
+
gem install pkg/murlsh-x.x.x.gem
|
21
|
+
</code>
|
22
|
+
</pre>
|
23
|
+
|
24
|
+
In the web directory:
|
25
|
+
|
26
|
+
<pre>
|
27
|
+
<code>
|
28
|
+
murlsh
|
29
|
+
edit config.yaml
|
30
|
+
rake db:init user:add
|
31
|
+
</code>
|
32
|
+
</pre>
|
data/Rakefile
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
require 'murlsh'
|
6
|
+
|
7
|
+
require 'flog'
|
8
|
+
require 'rake/testtask'
|
9
|
+
require 'sqlite3'
|
10
|
+
|
11
|
+
require 'pp'
|
12
|
+
require 'yaml'
|
13
|
+
|
14
|
+
config = YAML.load_file('config.yaml')
|
15
|
+
|
16
|
+
desc "Test remote content type fetch for a URL and show errors."
|
17
|
+
task :content_type, :url do |t, args|
|
18
|
+
puts Murlsh.get_content_type(args.url, :failproof => false)
|
19
|
+
end
|
20
|
+
|
21
|
+
namespace :db do
|
22
|
+
|
23
|
+
desc 'Delete the last url added.'
|
24
|
+
task :delete_last_url do
|
25
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
|
26
|
+
:database => config.fetch('db_file'))
|
27
|
+
|
28
|
+
last = Murlsh::Url.find(:last, :order => 'time')
|
29
|
+
pp last
|
30
|
+
response = ask('Delete this url', '?')
|
31
|
+
last.destroy if %w{y yes}.include?(response.downcase)
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Check for duplicate URLs."
|
35
|
+
task :dupcheck do
|
36
|
+
db = SQLite3::Database.new(config.fetch('db_file'))
|
37
|
+
db.results_as_hash = true
|
38
|
+
h = {}
|
39
|
+
db.execute("SELECT * FROM urls").each do |r|
|
40
|
+
h[r['url']] = h.fetch(r['url'], []).push([r['id'], r['time']])
|
41
|
+
end
|
42
|
+
h.select { |k,v| v.size > 1 }.each do |k,v|
|
43
|
+
puts k
|
44
|
+
v.each { |id,time| puts " #{id} #{time}" }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Create an empty database."
|
49
|
+
task :init do
|
50
|
+
puts "creating #{config.fetch('db_file')}"
|
51
|
+
db = SQLite3::Database.new(config.fetch('db_file'))
|
52
|
+
db.execute("CREATE TABLE urls (
|
53
|
+
id INTEGER PRIMARY KEY,
|
54
|
+
time TIMESTAMP,
|
55
|
+
url TEXT,
|
56
|
+
email TEXT,
|
57
|
+
name TEXT,
|
58
|
+
title TEXT,
|
59
|
+
content_type TEXT);
|
60
|
+
")
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Interact with the database.'
|
64
|
+
task :shell do
|
65
|
+
exec "sqlite3 #{config['db_file']}"
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
namespace :dreamhost do
|
71
|
+
|
72
|
+
desc "Restart Passenger."
|
73
|
+
task :restart do
|
74
|
+
open('tmp/restart.txt', 'w') { |f| }
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "Run flog on ruby and report on complexity."
|
80
|
+
task :flog do
|
81
|
+
flog = Flog.new
|
82
|
+
flog.flog('lib')
|
83
|
+
flog.report
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "Run test suite."
|
87
|
+
Rake::TestTask.new do |t|
|
88
|
+
t.pattern = 'test/*_test.rb'
|
89
|
+
t.verbose = true
|
90
|
+
t.warning = true
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "Test remote title fetch for a URL and show errors."
|
94
|
+
task :title, :url do |t, args|
|
95
|
+
puts Murlsh.get_title(args.url, :failproof => false)
|
96
|
+
end
|
97
|
+
|
98
|
+
desc 'Try to fetch the title for a url and update it in the database.'
|
99
|
+
task :title_fetch, :url_id do |t, args|
|
100
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
|
101
|
+
:database => config.fetch('db_file'))
|
102
|
+
url = Murlsh::Url.find(args.url_id)
|
103
|
+
puts "Url: #{url.url}"
|
104
|
+
puts "Previous title: #{url.title}"
|
105
|
+
url.title = Murlsh.get_title(url.url, :failproof => false)
|
106
|
+
url.save
|
107
|
+
puts "\nNew title: #{url.title}"
|
108
|
+
end
|
109
|
+
|
110
|
+
namespace :user do
|
111
|
+
|
112
|
+
desc "Add a new user."
|
113
|
+
task :add do
|
114
|
+
puts "adding to #{config.fetch('auth_file')}"
|
115
|
+
username = ask(:username)
|
116
|
+
email = ask(:email)
|
117
|
+
password = ask(:password)
|
118
|
+
|
119
|
+
Murlsh::Auth.new(config.fetch('auth_file')).add_user(username, email,
|
120
|
+
password)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
desc "Validate XHTML."
|
126
|
+
task :validate do
|
127
|
+
require 'cgi'
|
128
|
+
require 'net/http'
|
129
|
+
|
130
|
+
net_http = Net::HTTP.new('validator.w3.org', 80)
|
131
|
+
#net_http.set_debug_output(STDOUT)
|
132
|
+
|
133
|
+
check_url = config.fetch('root_url')
|
134
|
+
|
135
|
+
print "validating #{check_url} : "
|
136
|
+
|
137
|
+
net_http.start do |http|
|
138
|
+
resp = http.request_head(
|
139
|
+
"/check?uri=#{CGI::escape(check_url)}&charset=(detect+automatically)&doctype=Inline&group=0")
|
140
|
+
result = resp['X-W3C-Validator-Status']
|
141
|
+
errors = resp['X-W3C-Validator-Errors']
|
142
|
+
warnings = resp['X-W3C-Validator-Warnings']
|
143
|
+
|
144
|
+
puts "#{result} (#{errors} errors, #{warnings} warnings)"
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
desc 'Generate a shell script that will post a new url.'
|
150
|
+
task :post_sh do
|
151
|
+
puts <<EOS
|
152
|
+
#!/bin/sh
|
153
|
+
|
154
|
+
URL="$1"
|
155
|
+
AUTH="$2" # password can be passed as second parameter or hardcoded here
|
156
|
+
|
157
|
+
curl \\
|
158
|
+
--data-urlencode "url=${URL}" \\
|
159
|
+
--data-urlencode "auth=${AUTH}" \\
|
160
|
+
#{config.fetch('root_url')}
|
161
|
+
EOS
|
162
|
+
end
|
163
|
+
|
164
|
+
def ask(prompt, sep=':')
|
165
|
+
print "#{prompt}#{sep} "
|
166
|
+
return STDIN.gets.chomp
|
167
|
+
end
|
168
|
+
|
169
|
+
begin
|
170
|
+
require 'jeweler'
|
171
|
+
Jeweler::Tasks.new do |gemspec|
|
172
|
+
gemspec.name = 'murlsh'
|
173
|
+
gemspec.summary = 'url sharing site framework'
|
174
|
+
gemspec.description = 'url sharing site framework with easy adding, title lookup, atom feed, thumbnails and embedding'
|
175
|
+
gemspec.email = 'matthewm@boedicker.org'
|
176
|
+
gemspec.homepage = 'http://github.com/mmb/murlsh'
|
177
|
+
gemspec.authors = ['Matthew M. Boedicker']
|
178
|
+
|
179
|
+
%w{
|
180
|
+
activerecord 2.3.4
|
181
|
+
bcrypt 2.1.2
|
182
|
+
builder 2.1.2
|
183
|
+
hpricot 0.8.1
|
184
|
+
htmlentities 4.2.0
|
185
|
+
rack 1.0.1
|
186
|
+
sqlite3 1.2.5
|
187
|
+
}.each_slice(2) { |g,v| gemspec.add_dependency(g, ">= #{v}") }
|
188
|
+
|
189
|
+
end
|
190
|
+
rescue LoadError
|
191
|
+
puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
|
192
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.1
|
data/bin/murlsh
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
FileUtils.cp_r(
|
6
|
+
%w{.htaccess config.ru config.yaml plugins/ public/ Rakefile}.collect { |x|
|
7
|
+
File.join(File.dirname(__FILE__), '..', x) }, '.', :verbose => true)
|
8
|
+
|
9
|
+
FileUtils.mkdir('tmp')
|
10
|
+
|
11
|
+
puts <<eos
|
12
|
+
Next steps:
|
13
|
+
|
14
|
+
edit config.yaml
|
15
|
+
rake db:init user:add
|
16
|
+
eos
|
data/config.ru
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
2
|
+
|
3
|
+
require 'murlsh'
|
4
|
+
|
5
|
+
# use Rack::ShowExceptions
|
6
|
+
use Rack::ConditionalGet
|
7
|
+
use Rack::Deflater
|
8
|
+
use Rack::Static, :urls => %w{/css /js /swf}, :root => 'public'
|
9
|
+
use Rack::Static, :urls => %w{/atom.xml}
|
10
|
+
|
11
|
+
run Murlsh::Dispatch.new
|
data/config.yaml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
auth_file: /home/mmb/src/murlsh/murlsh_users
|
3
|
+
description: URLs found interesting by Matthew M. Boedicker
|
4
|
+
db_file: murlsh.db
|
5
|
+
feed_file: atom.xml
|
6
|
+
google_verify:
|
7
|
+
gravatar_size: 32
|
8
|
+
num_posts_feed: 25
|
9
|
+
num_posts_page: 100
|
10
|
+
page_title: mmb url share
|
11
|
+
root_url: http://urls.matthewm.boedicker.org/
|
12
|
+
css_prefix: css/
|
13
|
+
img_prefix: img/
|
14
|
+
js_prefix: js/
|
15
|
+
swf_prefix: swf/
|
16
|
+
css_files:
|
17
|
+
- jquery.jgrowl.css
|
18
|
+
- screen.css
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Murlsh
|
7
|
+
|
8
|
+
class AtomFeed
|
9
|
+
|
10
|
+
def initialize(root_url, options={})
|
11
|
+
options = {
|
12
|
+
:filename => 'atom.xml',
|
13
|
+
:title => 'Atom feed' }.merge(options)
|
14
|
+
@root_url = root_url
|
15
|
+
@filename = options[:filename]
|
16
|
+
@title = options[:title]
|
17
|
+
|
18
|
+
setup_id_fields
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup_id_fields
|
22
|
+
uri_parsed = URI(@root_url)
|
23
|
+
|
24
|
+
m = uri_parsed.host.match(/^(.*?)\.?([^.]+\.[^.]+)$/)
|
25
|
+
|
26
|
+
@host, @domain = (m ? m.captures : [uri_parsed.host, ''])
|
27
|
+
|
28
|
+
@path = uri_parsed.path
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(entries, path)
|
32
|
+
open(path, 'w') do |f|
|
33
|
+
f.flock(File::LOCK_EX)
|
34
|
+
|
35
|
+
make(entries, :target => f)
|
36
|
+
|
37
|
+
f.flock(File::LOCK_UN)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def make(entries, options={})
|
42
|
+
xm = Builder::XmlMarkup.new(options)
|
43
|
+
xm.instruct! :xml
|
44
|
+
|
45
|
+
xm.feed(:xmlns => 'http://www.w3.org/2005/Atom') {
|
46
|
+
xm.id(@root_url)
|
47
|
+
xm.link(:href => URI.join(@root_url, @filename), :rel => 'self')
|
48
|
+
xm.title(@title)
|
49
|
+
xm.updated(entries.collect { |mu| mu.time }.max.xmlschema)
|
50
|
+
entries.each do |mu|
|
51
|
+
xm.entry {
|
52
|
+
xm.author { xm.name(mu.name) }
|
53
|
+
xm.title(mu.title)
|
54
|
+
xm.id(entry_id(mu))
|
55
|
+
xm.summary(mu.title)
|
56
|
+
xm.updated(mu.time.xmlschema)
|
57
|
+
xm.link(:href => mu.url)
|
58
|
+
enclosure(xm, mu)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
}
|
62
|
+
xm
|
63
|
+
end
|
64
|
+
|
65
|
+
def entry_id(url)
|
66
|
+
"tag:#{@domain},#{url.time.strftime('%Y-%m-%d')}:#{@host}#{@path}#{url.id}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def enclosure(xm, mu)
|
70
|
+
xm.link(:rel => 'enclosure', :type => mu.content_type, :href => mu.url,
|
71
|
+
:title => 'Full-size') if mu.is_image?
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/murlsh/auth.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bcrypt'
|
3
|
+
|
4
|
+
require 'csv'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Murlsh
|
8
|
+
|
9
|
+
class Auth
|
10
|
+
|
11
|
+
def initialize(file)
|
12
|
+
@file = file
|
13
|
+
end
|
14
|
+
|
15
|
+
def auth(password)
|
16
|
+
CSV::Reader.parse(open(@file)) do |row|
|
17
|
+
return { :name => row[0], :email => row[1] } if
|
18
|
+
BCrypt::Password.new(row[2]) == password
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_user(username, email, password)
|
23
|
+
open(@file, 'a') do |f|
|
24
|
+
f.flock(File::LOCK_EX)
|
25
|
+
f.write("#{[username, Digest::MD5.hexdigest(email),
|
26
|
+
BCrypt::Password.create(password)].join(',')}\n")
|
27
|
+
f.flock(File::LOCK_UN)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
%w{
|
2
|
+
murlsh
|
3
|
+
|
4
|
+
rubygems
|
5
|
+
active_record
|
6
|
+
rack
|
7
|
+
sqlite3
|
8
|
+
|
9
|
+
yaml
|
10
|
+
}.each { |m| require m }
|
11
|
+
|
12
|
+
module Murlsh
|
13
|
+
|
14
|
+
class Dispatch
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@config = YAML.load_file('config.yaml')
|
18
|
+
@url_root = URI(@config.fetch('root_url')).path
|
19
|
+
|
20
|
+
ActiveRecord::Base.establish_connection(
|
21
|
+
:adapter => 'sqlite3', :database => @config.fetch('db_file'))
|
22
|
+
|
23
|
+
@db = ActiveRecord::Base.connection.instance_variable_get(:@connection)
|
24
|
+
|
25
|
+
@url_server = Murlsh::UrlServer.new(@config, @db)
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(env)
|
29
|
+
dispatch = {
|
30
|
+
['GET', @url_root] => [@url_server, :get],
|
31
|
+
['POST', @url_root] => [@url_server, :post],
|
32
|
+
['GET', "#{@url_root}url"] => [@url_server, :get],
|
33
|
+
['POST', "#{@url_root}url"] => [@url_server, :post],
|
34
|
+
}
|
35
|
+
dispatch.default = [self, :not_found]
|
36
|
+
|
37
|
+
req = Rack::Request.new(env)
|
38
|
+
|
39
|
+
obj, meth = dispatch[[req.request_method, req.path]]
|
40
|
+
|
41
|
+
obj.send(meth, req).finish
|
42
|
+
end
|
43
|
+
|
44
|
+
def not_found(req)
|
45
|
+
Rack::Response.new("<p>#{req.url} not found</p>
|
46
|
+
|
47
|
+
<p><a href=\"#{@config['root_url']}\">root<a></p>
|
48
|
+
",
|
49
|
+
404, { 'Content-Type' => 'text/html' })
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
class URI::Generic
|
6
|
+
|
7
|
+
def path_query
|
8
|
+
path + (query ? "?#{query}" : '')
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
module Murlsh
|
14
|
+
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def get_content_type(url, options={})
|
18
|
+
options[:headers] = default_headers(url).merge(
|
19
|
+
options.fetch(:headers, {}))
|
20
|
+
|
21
|
+
options = {
|
22
|
+
:failproof => true,
|
23
|
+
:redirects => 0,
|
24
|
+
}.merge(options)
|
25
|
+
|
26
|
+
unless options[:redirects] > 3
|
27
|
+
begin
|
28
|
+
url = parse_uri(url)
|
29
|
+
|
30
|
+
make_net_http(url, options).start do |http|
|
31
|
+
resp = get_resp(http, url, options[:headers])
|
32
|
+
case resp
|
33
|
+
when Net::HTTPSuccess then return resp['content-type']
|
34
|
+
when Net::HTTPRedirection then
|
35
|
+
options[:redirects] += 1
|
36
|
+
return get_content_type(resp['location'], options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
rescue Exception => e
|
40
|
+
raise unless options[:failproof]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
''
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parse a URI if it's not already parsed.
|
47
|
+
def parse_uri(uri)
|
48
|
+
uri.is_a?(URI::HTTP) ? uri : URI(uri)
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_net_http(url, options={})
|
52
|
+
net_http = Net::HTTP.new(url.host, url.port)
|
53
|
+
net_http.use_ssl = (url.scheme == 'https')
|
54
|
+
net_http.set_debug_output(options[:debug]) if options[:debug]
|
55
|
+
net_http
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the response to HTTP HEAD. If HEAD not allowed do GET.
|
59
|
+
def get_resp(http, url, headers={})
|
60
|
+
resp = http.request_head(url.path_query, headers)
|
61
|
+
if Net::HTTPMethodNotAllowed === resp
|
62
|
+
http.request_get(url.path_query, headers)
|
63
|
+
else
|
64
|
+
resp
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_headers(url)
|
69
|
+
result = {
|
70
|
+
'User-Agent' =>
|
71
|
+
'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4) Gecko/20030624',
|
72
|
+
}
|
73
|
+
begin
|
74
|
+
parsed_url = parse_uri(url)
|
75
|
+
if (parsed_url.host || '')[/^www\.nytimes\.com/]
|
76
|
+
result['Referer'] = 'http://news.google.com/'
|
77
|
+
end
|
78
|
+
rescue URI::InvalidURIError => e
|
79
|
+
end
|
80
|
+
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hpricot'
|
3
|
+
require 'htmlentities'
|
4
|
+
|
5
|
+
require 'iconv'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
module Murlsh
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def get_title(url, options={})
|
14
|
+
options[:headers] = default_headers(url).merge(
|
15
|
+
options.fetch(:headers, {}))
|
16
|
+
|
17
|
+
options = {
|
18
|
+
:failproof => true,
|
19
|
+
}.merge(options)
|
20
|
+
|
21
|
+
result = nil
|
22
|
+
begin
|
23
|
+
options[:content_type] ||= get_content_type(url, options)
|
24
|
+
if might_have_title(options[:content_type])
|
25
|
+
f = open(url, options[:headers])
|
26
|
+
|
27
|
+
doc = Hpricot(f)
|
28
|
+
|
29
|
+
result = HTMLEntities.new.decode(Iconv.conv('utf-8',
|
30
|
+
get_charset(doc) || f.charset, find_title(doc)))
|
31
|
+
end
|
32
|
+
rescue Exception => e
|
33
|
+
raise unless options[:failproof]
|
34
|
+
end
|
35
|
+
(result and !result.empty?) ? result : url
|
36
|
+
end
|
37
|
+
|
38
|
+
def might_have_title(content_type)
|
39
|
+
content_type[/^text\/html/]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Find the title in an Hpricot document.
|
43
|
+
def find_title(doc)
|
44
|
+
%w{//html/head/title //head/title //html/title //title}.each do |xpath|
|
45
|
+
return (doc/xpath).first.inner_html unless (doc/xpath).first.nil?
|
46
|
+
end
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the character set of an Hpricot document.
|
51
|
+
def get_charset(doc)
|
52
|
+
%w{content-type Content-Type}.each do |ct|
|
53
|
+
content_type = doc.at("meta[@http-equiv='#{ct}']")
|
54
|
+
unless content_type.nil?
|
55
|
+
content = content_type['content']
|
56
|
+
unless content.nil?
|
57
|
+
charset = content[/charset=([\w_.:-]+)/, 1]
|
58
|
+
return charset if charset
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Murlsh
|
2
|
+
|
3
|
+
module Markup
|
4
|
+
|
5
|
+
def javascript(sources, options={})
|
6
|
+
sources.to_a.each do |src|
|
7
|
+
script('', :type => 'text/javascript',
|
8
|
+
:src => "#{options[:prefix]}#{src}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def murlsh_img(options={})
|
13
|
+
img_convert_prefix(options)
|
14
|
+
img_convert_size(options)
|
15
|
+
img_convert_text(options)
|
16
|
+
|
17
|
+
if options[:href]
|
18
|
+
a(:href => options[:href]) {
|
19
|
+
options.delete(:href)
|
20
|
+
img(options)
|
21
|
+
}
|
22
|
+
else
|
23
|
+
img(options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def atom(href)
|
28
|
+
link(:rel => 'alternate', :type => 'application/atom+xml', :href => href)
|
29
|
+
end
|
30
|
+
|
31
|
+
def css(hrefs, options={})
|
32
|
+
hrefs.to_a.each do |href|
|
33
|
+
attrs = {
|
34
|
+
:href => "#{options[:prefix]}#{href}",
|
35
|
+
:rel => 'stylesheet',
|
36
|
+
:type => 'text/css',
|
37
|
+
}
|
38
|
+
attrs[:media] = options[:media] if options[:media]
|
39
|
+
link(attrs)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def metas(tags)
|
44
|
+
tags.each { |k,v| meta(:name => k, :content => v) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def gravatar(email_hash, options={})
|
48
|
+
query = options.reject do |k,v|
|
49
|
+
not ((k == 'd' and %w{identicon monsterid wavatar}.include?(v)) or
|
50
|
+
(k =='s' and (0..512).include?(v)) or
|
51
|
+
(k == 'r' and %w{g pg r x}.include?(v)))
|
52
|
+
end
|
53
|
+
|
54
|
+
return if query['s'] and query['s'] < 1
|
55
|
+
|
56
|
+
options.reject! { |k,v| %w{d s r}.include?(k) }
|
57
|
+
options[:src] = URI.join('http://www.gravatar.com/avatar/',
|
58
|
+
email_hash + build_query(query))
|
59
|
+
|
60
|
+
murlsh_img(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_query(h)
|
64
|
+
h.empty? ? '' :
|
65
|
+
'?' + h.collect { |k,v| URI.escape("#{k}=#{v}") }.join('&')
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def img_convert_prefix(options)
|
71
|
+
if options.has_key?(:prefix) and options.has_key?(:src)
|
72
|
+
options[:src] = options[:prefix] + options[:src]
|
73
|
+
options.delete(:prefix)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def img_convert_size(options)
|
78
|
+
if options.has_key?(:size)
|
79
|
+
if options[:size].kind_of?(Array) and options[:size].size == 2
|
80
|
+
options[:width], options[:height] = options[:size]
|
81
|
+
else
|
82
|
+
options[:width] = options[:height] = options[:size]
|
83
|
+
end
|
84
|
+
options.delete(:size)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def img_convert_text(options)
|
89
|
+
if options.has_key?(:text)
|
90
|
+
options[:alt] = options[:title] = options[:text]
|
91
|
+
options.delete(:text)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|