murlsh 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.htaccess CHANGED
@@ -9,11 +9,13 @@ AddOutputFilterByType DEFLATE text/css
9
9
  AddOutputFilterByType DEFLATE text/html
10
10
 
11
11
  <FilesMatch "\.gen\.(css|js)$">
12
- Header add Expires "Wed, 22 Jun 2019 20:07:00 GMT"
12
+ ExpiresActive On
13
+ ExpiresDefault "now plus 1 years"
13
14
  FileETag None
14
15
  </FilesMatch>
15
16
 
16
17
  <FilesMatch "[\da-z]{32}\.(gif|jpe?g|png)$">
17
- Header add Expires "Wed, 22 Jun 2019 20:07:00 GMT"
18
+ ExpiresActive On
19
+ ExpiresDefault "now plus 1 years"
18
20
  FileETag None
19
21
  </FilesMatch>
data/README.textile CHANGED
@@ -4,14 +4,15 @@ Site for sharing and archiving links.
4
4
  * jGrowls embedded versions of Imageshack, Vimeo and YouTube urls
5
5
  * converts Twitter status urls to their full text and adds user thumbnail
6
6
  * generates Atom and RSS feeds
7
+ * generates json and jsonp feeds for client-side inclusion in other sites
7
8
  * regex search
8
- * embeds Flash mp3 player for mp3 urls
9
+ * uses HTML5 audio for mp3 and ogg urls
9
10
  * looks good on iPhone
10
11
  * PubSubHubbub notification
11
12
  * plugin interface
12
13
  * rack interface
13
14
  * Gravatar support
14
- * imports Netscape bookmark format files
15
+ * generates import scripts from delicious api exports
15
16
 
16
17
  See "http://urls.matthewm.boedicker.org/":http://urls.matthewm.boedicker.org/ for example.
17
18
 
@@ -32,6 +33,14 @@ rake init
32
33
  </code>
33
34
  </pre>
34
35
 
36
+ h2. Development
37
+
38
+ * Create a fork and check it out
39
+ * edit config.yaml
40
+ * rake init
41
+ * rackup
42
+ * open http://localhost:9292/
43
+
35
44
  h1. Updating
36
45
 
37
46
  If you are using the gem and it gets updated to a new version you should run
@@ -39,6 +48,16 @@ the murlsh command again from your web directory to update plugins, javascript
39
48
  and css. It will prompt before overwriting anything in case you have made
40
49
  modifications.
41
50
 
51
+ h1. API
52
+
53
+ h2. Recent urls
54
+
55
+ * http://your_root/atom.atom
56
+ * http://your_root/rss.rss
57
+ * http://your_root/podcast.rss (urls with audio/mpeg content type)
58
+ * http://your_root/json.json
59
+ * http://your_root/json.json?callback=x (jsonp)
60
+
42
61
  h1. Plugins
43
62
 
44
63
  Classes in the plugins directory can be used to change behavior at certain
data/Rakefile CHANGED
@@ -3,6 +3,7 @@ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
3
3
  require 'cgi'
4
4
  require 'digest/md5'
5
5
  require 'net/http'
6
+ require 'open-uri'
6
7
  require 'pp'
7
8
  require 'set'
8
9
  require 'uri'
@@ -131,17 +132,12 @@ end
131
132
 
132
133
  desc 'Run test suite.'
133
134
  begin
134
- require 'spec/rake/spectask'
135
+ require 'rspec/core/rake_task'
135
136
 
136
- Spec::Rake::SpecTask.new('test') do |t|
137
- t.spec_files = FileList['spec/*_spec.rb']
138
- t.spec_opts = %w{--color}
137
+ RSpec::Core::RakeTask.new('test') do |t|
138
+ t.pattern = 'spec/**/*_spec.rb'
139
+ t.rspec_opts = %w{--color}
139
140
  t.verbose = true
140
- t.warning = true
141
- end
142
- rescue LoadError
143
- task :test do
144
- gem_not_found 'rspec'
145
141
  end
146
142
  end
147
143
 
@@ -320,9 +316,17 @@ namespace :js do
320
316
 
321
317
  desc 'Run javascript through jslint.'
322
318
  task :jslint do
319
+ local_jslint = 'jslint_rhino.js'
320
+ open(local_jslint, 'w') do |f|
321
+ f.write(cat(%w{
322
+ https://github.com/AndyStricker/JSLint/raw/rhinocmdline/fulljslint.js
323
+ https://github.com/AndyStricker/JSLint/raw/rhinocmdline/rhino.js
324
+ }))
325
+ end
326
+
323
327
  MURLSH_JS.each do |jsf|
324
328
  puts jsf
325
- puts `rhino http://www.jslint.com/rhino/jslint.js #{jsf}`
329
+ puts `rhino #{local_jslint} #{jsf}`
326
330
  end
327
331
  end
328
332
 
@@ -455,7 +459,7 @@ begin
455
459
  %w{
456
460
  flog >= 2.5.0
457
461
  rack-test ~> 0.5
458
- rspec ~> 1.3
462
+ rspec ~> 2.0
459
463
  }.each_slice(3) do |g,o,v|
460
464
  gemspec.add_development_dependency(g, "#{o} #{v}")
461
465
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 1.3.0
data/bin/murlsh CHANGED
@@ -50,10 +50,11 @@ cp_r_safe(
50
50
 
51
51
  FileUtils.mkdir_p('tmp', :verbose => true)
52
52
 
53
+ cd_command = File.expand_path(dest_dir) == Dir.pwd ? '' : "cd #{dest_dir}\n"
54
+
53
55
  puts <<eos
54
56
  Next steps:
55
57
 
56
- cd #{dest_dir}
57
- edit config.yaml
58
+ #{cd_command}edit config.yaml
58
59
  rake init
59
60
  eos
data/config.ru CHANGED
@@ -31,7 +31,7 @@ use Murlsh::FarFutureExpires, :patterns => [
31
31
  feed_url = URI.join(config.fetch('root_url'), config.fetch('feed_file'))
32
32
  use Murlsh::MustRevalidate, :patterns => %r{^#{Regexp.escape(feed_url.path)}$}
33
33
 
34
- use Rack::Static, :urls => %w{/css /img /js /swf}, :root => 'public'
34
+ use Rack::Static, :urls => %w{/css/ /img/ /js/}, :root => 'public'
35
35
  use Rack::Static, :urls => %w{/atom.atom /podcast.rss /rss.rss}
36
36
 
37
37
  use Rack::Rewrite do
data/config.yaml CHANGED
@@ -2,8 +2,6 @@
2
2
  auth_file: murlsh_users
3
3
  cache_entitystore: file:tmp/cache/rack/body
4
4
  cache_metastore: file:tmp/cache/rack/meta
5
- config_js: []
6
-
7
5
  css_files:
8
6
  - css/jquery.jgrowl.css
9
7
  - css/screen.css
@@ -28,3 +26,9 @@ root_url: http://urls.matthewm.boedicker.org/
28
26
  show_names: true
29
27
  thumbnail_max_side: 90
30
28
  user_agent: murlsh (http://github.com/mmb/murlsh)
29
+ predefined_searches:
30
+ audio: \.(mp3|ogg)$
31
+ code: (code\.google|github)\.com/
32
+ images: \.(png|gif|jpe?g)$
33
+ tweets: twitter\.com/
34
+ video: (youtube|vimeo)\.com/
@@ -0,0 +1,12 @@
1
+ require 'digest/md5'
2
+
3
+ module Murlsh
4
+
5
+ module BuildMd5
6
+
7
+ # Return the md5 sum of the result of the build method.
8
+ def md5; Digest::MD5.hexdigest(build); end
9
+
10
+ end
11
+
12
+ end
@@ -15,15 +15,15 @@ module Murlsh
15
15
  @config = config
16
16
 
17
17
  url_server = Murlsh::UrlServer.new(config)
18
- config_server = Murlsh::ConfigServer.new(config)
18
+ json_server = Murlsh::JsonServer.new(config)
19
19
  root_path = URI(config.fetch('root_url')).path
20
20
 
21
21
  @routes = [
22
22
  [%r{^HEAD #{root_path}(url)?$}, url_server.method(:head)],
23
23
  [%r{^GET #{root_path}(url)?$}, url_server.method(:get)],
24
24
  [%r{^POST #{root_path}(url)?$}, url_server.method(:post)],
25
- [%r{^HEAD #{root_path}config$}, config_server.method(:head)],
26
- [%r{^GET #{root_path}config$}, config_server.method(:get)],
25
+ [%r{^HEAD #{root_path}json\.json$}, json_server.method(:head)],
26
+ [%r{^GET #{root_path}json\.json$}, json_server.method(:get)],
27
27
  ]
28
28
 
29
29
  db_init
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  require 'rack'
2
4
  require 'rack/utils'
3
5
 
@@ -10,7 +12,9 @@ module Murlsh
10
12
  def initialize(app, options={})
11
13
  @app = app
12
14
  @patterns = options[:patterns] ? [*options[:patterns]] : []
13
- @future = options[:future] || 'Wed, 22 Jun 2019 20:07:00 GMT'
15
+ # rfc2616 HTTP/1.1 servers SHOULD NOT send Expires dates more than one
16
+ # year in the future.
17
+ @future = options[:future] || (Time.now + 31536000).httpdate
14
18
  end
15
19
 
16
20
  def call(env)
@@ -0,0 +1,41 @@
1
+ require 'uri'
2
+
3
+ module Murlsh
4
+
5
+ # Recent urls json response builder.
6
+ class JsonBody
7
+ include Murlsh::BuildMd5
8
+
9
+ def initialize(config, req, result_set)
10
+ @config, @req, @result_set = config, req, result_set
11
+ end
12
+
13
+ # Yield body for Rack.
14
+ def each; yield build; end
15
+
16
+ # Recent urls json response builder.
17
+ def build
18
+ if defined?(@body)
19
+ @body
20
+ else
21
+ urls = @result_set.results.map do |mu|
22
+ h = mu.attributes
23
+
24
+ h['title'] = mu.title_stripped
25
+
26
+ # add site root url to relative thumbnail urls
27
+ if h['thumbnail_url'] and
28
+ not URI(h['thumbnail_url']).scheme.to_s.downcase[/https?/]
29
+ h['thumbnail_url'] = URI.join(@config['root_url'],
30
+ h['thumbnail_url']).to_s
31
+ end
32
+
33
+ h
34
+ end
35
+ @body = urls.to_json
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,39 @@
1
+ require 'rack'
2
+
3
+ module Murlsh
4
+
5
+ # Serve most recent urls in json and jsonp.
6
+ class JsonServer
7
+
8
+ include Murlsh::HeadFromGet
9
+
10
+ def initialize(config); @config = config; end
11
+
12
+ # Respond to a GET request. Return json of recent urls or jsonp if
13
+ # if callback parameter is sent.
14
+ def get(req)
15
+ conditions = Murlsh::SearchConditions.new(req['q']).conditions
16
+ page = 1
17
+ per_page = @config.fetch('num_posts_feed', 25)
18
+
19
+ result_set = Murlsh::UrlResultSet.new(conditions, page, per_page)
20
+
21
+ resp = Rack::Response.new
22
+
23
+ if req['callback']
24
+ resp['Content-Type'] = 'application/javascript'
25
+ resp.body = Murlsh::JsonpBody.new(@config, req, result_set)
26
+ else
27
+ resp['Content-Type'] = 'application/json'
28
+ resp.body = Murlsh::JsonBody.new(@config, req, result_set)
29
+ end
30
+
31
+ resp['Cache-Control'] = 'must-revalidate, max-age=0'
32
+ resp['ETag'] = "\"#{resp.body.md5}\""
33
+
34
+ resp
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,17 @@
1
+ module Murlsh
2
+
3
+ # Recent urls jsonp response builder.
4
+ class JsonpBody < Murlsh::JsonBody
5
+
6
+ # Recent urls jsonp response builder.
7
+ def build
8
+ if defined?(@body)
9
+ @body
10
+ else
11
+ @body = "#{@req['callback']}(#{super})"
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,22 @@
1
+ module Murlsh
2
+
3
+ # Search conditions builder for ActiveRecord conditions.
4
+ class SearchConditions
5
+
6
+ def initialize(q); @q = q; end
7
+
8
+ # Search conditions builder for ActiveRecord conditions.
9
+ def conditions
10
+ if q
11
+ search_cols = %w{name title url}
12
+ [search_cols.map { |x| "MURLSHMATCH(#{x}, ?)" }.join(' OR ')].push(
13
+ *[q] * search_cols.size)
14
+ else
15
+ []
16
+ end
17
+ end
18
+
19
+ attr_accessor :q
20
+ end
21
+
22
+ end
data/lib/murlsh/url.rb CHANGED
@@ -36,6 +36,8 @@ module Murlsh
36
36
  @ask
37
37
  end
38
38
 
39
+ attr_accessor :user_supplied_title
40
+ alias :user_supplied_title? :user_supplied_title
39
41
  end
40
42
 
41
43
  end
@@ -1,119 +1,98 @@
1
1
  require 'builder'
2
+ require 'uri'
2
3
 
3
4
  module Murlsh
4
5
 
5
6
  # Url list page builder.
6
7
  class UrlBody < Builder::XmlMarkup
8
+ include Murlsh::BuildMd5
7
9
  include Murlsh::Markup
8
10
 
9
- def initialize(config, req, content_type='text/html')
10
- @config, @req, @q, @content_type =
11
- config, req, req.params['q'], content_type
12
- @page = [req.params['p'].to_i, 1].max
13
-
14
- @first_href, @prev_href, @next_href = page_href(1), nil, nil
15
-
16
- @total_entries, @total_pages = 0, 0
17
-
18
- @per_page = @req.params['pp'] ? @req.params['pp'].to_i :
19
- config.fetch('num_posts_page', 25)
20
-
11
+ def initialize(config, req, result_set, content_type='text/html')
12
+ @config, @req, @result_set, @content_type = config, req, result_set,
13
+ content_type
21
14
  super(:indent => @config['html_indent'] || 0)
22
15
  end
23
16
 
24
17
  # Get the href of a page in the same result set as this page.
18
+ #
19
+ # Return nil if page is invalid.
25
20
  def page_href(page)
26
- query = @req.params.dup
27
- query['p'] = page
28
- Murlsh.build_query(query)
29
- end
30
-
31
- # Fetch urls based on query string parameters.
32
- def urls
33
- search = search_conditions
34
-
35
- @total_entries = Murlsh::Url.count(:conditions => search)
36
- @total_pages = (@total_entries / @per_page.to_f).ceil
37
-
38
- if @page > 1 and @page <= @total_pages
39
- @prev_href = page_href(@page - 1)
40
- end
41
-
42
- if @page < @total_pages
43
- @next_href = page_href(@page + 1)
44
- end
45
-
46
- offset = (@page - 1) * @per_page
47
-
48
- Murlsh::Url.all(:conditions => search_conditions, :order => 'time DESC',
49
- :limit => @per_page, :offset => offset)
50
- end
51
-
52
- # Search conditions builder for ActiveRecord conditions.
53
- def search_conditions
54
- if @q
55
- search_cols = %w{name title url}
56
- [search_cols.map { |x| "MURLSHMATCH(#{x}, ?)" }.join(' OR ')].push(
57
- *[@q] * search_cols.size)
58
- else
59
- []
21
+ if page.to_i >= 1
22
+ query = @req.params.dup
23
+ query['p'] = page
24
+ Murlsh.build_query(query)
60
25
  end
61
26
  end
62
27
 
63
- # Url list page body builder.
64
- def each
65
- mus = urls
28
+ # Get the url of the previous page or nil if this is the first.
29
+ def prev_href; page_href(@result_set.prev_page); end
66
30
 
67
- declare! :DOCTYPE, :html
31
+ # Get the url of the next page or nil if this is the last.
32
+ def next_href; page_href(@result_set.next_page); end
68
33
 
69
- yield html(:lang => 'en') {
70
- headd
71
- body {
72
- ul(:id => 'urls') {
73
- li { feed_icon ; search_form }
34
+ # Yield body for Rack.
35
+ def each; yield build; end
74
36
 
75
- last = nil
37
+ # Url list page body builder.
38
+ def build
39
+ if defined?(@body)
40
+ @body
41
+ else
42
+ declare! :DOCTYPE, :html
43
+
44
+ @body = html(:lang => 'en') {
45
+ headd
46
+ body {
47
+ search_form
48
+ self.p {
49
+ predefined_searches
50
+ feed_link
51
+ }
52
+ ul(:id => 'urls') {
53
+ last = nil
54
+
55
+ @result_set.results.each do |mu|
56
+ li {
57
+ unless mu.same_author?(last)
58
+ avatar_url = Murlsh::Plugin.hooks('avatar').inject(
59
+ nil) do |url_so_far,plugin|
60
+ plugin.run(url_so_far, mu, @config)
61
+ end
62
+ div(:class => 'icon') {
63
+ murlsh_img :src => avatar_url, :text => mu.name
64
+ } if avatar_url
65
+ div(mu.name, :class => 'name') if
66
+ @config.fetch('show_names', false) and mu.name
67
+ end
76
68
 
77
- mus.each do |mu|
78
- li {
79
- unless mu.same_author?(last)
80
- avatar_url = Murlsh::Plugin.hooks('avatar').inject(
81
- nil) do |url_so_far,plugin|
82
- plugin.run(url_so_far, mu, @config)
69
+ if mu.thumbnail_url
70
+ murlsh_img :src => mu.thumbnail_url,
71
+ :text => mu.title_stripped, :class => 'thumb'
83
72
  end
84
- div(:class => 'icon') {
85
- murlsh_img :src => avatar_url, :text => mu.name
86
- } if avatar_url
87
- div(mu.name, :class => 'name') if
88
- @config.fetch('show_names', false) and mu.name
89
- end
90
73
 
91
- if mu.thumbnail_url
92
- murlsh_img :src => mu.thumbnail_url,
93
- :text => mu.title_stripped, :class => 'thumb'
94
- end
74
+ a mu.title_stripped, :href => mu.url, :class => 'm'
95
75
 
96
- a mu.title_stripped, :href => mu.url, :class => 'm'
76
+ Murlsh::Plugin.hooks('url_display_add') do |p|
77
+ p.run self, mu, @config
78
+ end
97
79
 
98
- Murlsh::Plugin.hooks('url_display_add') do |p|
99
- p.run self, mu, @config
100
- end
80
+ last = mu
81
+ }
82
+ end
83
+ }
101
84
 
102
- last = mu
103
- }
104
- end
85
+ clear
105
86
 
106
- li { paging_nav }
87
+ paging_nav
88
+ add_form
89
+ powered_by
107
90
 
108
- li { add_form }
91
+ js
92
+ div '', :id => 'bottom'
109
93
  }
110
-
111
- clear
112
- powered_by
113
- js
114
- div '', :id => 'bottom'
115
94
  }
116
- }
95
+ end
117
96
  end
118
97
 
119
98
  # Head builder.
@@ -125,29 +104,39 @@ module Murlsh
125
104
  map { |k,v| [k.sub('meta_tag_', ''), v] })
126
105
  css(@config['css_compressed'] || @config['css_files'])
127
106
  atom @config.fetch('feed_file')
128
- link :rel => 'first', :href => @first_href
107
+ link :rel => 'first', :href => page_href(1)
129
108
  link :rel => 'prev', :href => @prev_href if @prev_href
130
109
  link :rel => 'next', :href => @next_href if @next_href
131
110
  }
132
111
  end
133
112
 
113
+ # Predefined search list builder.
114
+ def predefined_searches
115
+ if @config['predefined_searches']
116
+ text! 'search: '
117
+ @config['predefined_searches'].each do |k,v|
118
+ a "/#{k}", :href => "?q=#{URI.escape(v)}" ; text! ' '
119
+ end
120
+ text! '| '
121
+ end
122
+ end
123
+
134
124
  # Title builder.
135
125
  def titlee
136
- title(@config.fetch('page_title', '') + (@q ? " /#{@q}" : ''))
126
+ title(@config.fetch('page_title', '') +
127
+ (@req['q'] ? " /#{@req['q']}" : ''))
137
128
  end
138
129
 
139
- # Feed icon builder.
140
- def feed_icon
141
- div(:class => 'icon') {
142
- a('feed', :href => @config.fetch('feed_file'), :class => 'feed')
143
- }
130
+ # Feed link builder.
131
+ def feed_link
132
+ a('feed', :href => @config.fetch('feed_file'), :class => 'feed')
144
133
  end
145
134
 
146
135
  # Search form builder.
147
136
  def search_form
148
137
  form(:action => '', :method => 'get') {
149
138
  fieldset {
150
- form_input :id => 'q', :size => 32, :value => @q
139
+ form_input :id => 'q', :size => 32, :value => @req['q']
151
140
  form_input :type => 'submit', :value => 'Regex Search'
152
141
  }
153
142
  }
@@ -155,13 +144,15 @@ module Murlsh
155
144
 
156
145
  # Paging navigation.
157
146
  def paging_nav
158
- text! "Page #{@page}/#{@total_pages}"
159
- if @prev_href
160
- text! ' | '; a 'previous', :href => @prev_href
161
- end
162
- if @next_href
163
- text! ' | '; a 'next', :href => @next_href
164
- end
147
+ self.p {
148
+ text! "Page #{@result_set.page}/#{@result_set.total_pages}"
149
+ if p_href = prev_href
150
+ text! ' | '; a 'previous', :href => p_href
151
+ end
152
+ if n_href = next_href
153
+ text! ' | '; a 'next', :href => n_href
154
+ end
155
+ }
165
156
  end
166
157
 
167
158
  # Url add form builder.
@@ -0,0 +1,35 @@
1
+ module Murlsh
2
+
3
+ class UrlResultSet
4
+
5
+ def initialize(conditions, page, per_page)
6
+ @conditions, @page, @per_page = conditions, page, per_page
7
+ @order = 'time DESC'
8
+ end
9
+
10
+ def total_entries
11
+ @total_entries ||= Murlsh::Url.count(:conditions => conditions)
12
+ end
13
+
14
+ def total_pages
15
+ @total_pages ||= [(total_entries / per_page.to_f).ceil, 1].max
16
+ end
17
+
18
+ def offset; @offset ||= (page - 1) * per_page; end
19
+
20
+ def results
21
+ Murlsh::Url.all(:conditions => conditions, :order => order,
22
+ :limit => per_page, :offset => offset)
23
+ end
24
+
25
+ def prev_page; @prev_page ||= page - 1 if (2..total_pages) === page; end
26
+
27
+ def next_page; @next_page ||= page + 1 if page < total_pages; end
28
+
29
+ attr_reader :conditions
30
+ attr_reader :page
31
+ attr_reader :per_page
32
+ attr_reader :order
33
+ end
34
+
35
+ end