whisperblog 0.6

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.
Files changed (48) hide show
  1. data/Changelog +23 -0
  2. data/LICENSE +661 -0
  3. data/README +59 -0
  4. data/bin/whisper +91 -0
  5. data/bin/whisper-init +40 -0
  6. data/bin/whisper-post +88 -0
  7. data/bin/whisper-process-email +54 -0
  8. data/lib/whisper.rb +52 -0
  9. data/lib/whisper/author_tracker.rb +46 -0
  10. data/lib/whisper/blog.rb +156 -0
  11. data/lib/whisper/cached_file.rb +65 -0
  12. data/lib/whisper/comment.rb +35 -0
  13. data/lib/whisper/comment_set.rb +41 -0
  14. data/lib/whisper/common.rb +276 -0
  15. data/lib/whisper/config.rb +49 -0
  16. data/lib/whisper/dir_scanner.rb +76 -0
  17. data/lib/whisper/dir_set.rb +50 -0
  18. data/lib/whisper/email_receiver.rb +212 -0
  19. data/lib/whisper/email_sender.rb +114 -0
  20. data/lib/whisper/entry.rb +44 -0
  21. data/lib/whisper/entry_set.rb +41 -0
  22. data/lib/whisper/handler.rb +99 -0
  23. data/lib/whisper/mbox.rb +141 -0
  24. data/lib/whisper/page.rb +118 -0
  25. data/lib/whisper/rfc2047.rb +79 -0
  26. data/lib/whisper/router.rb +50 -0
  27. data/lib/whisper/server.rb +88 -0
  28. data/lib/whisper/text.rb +252 -0
  29. data/lib/whisper/timed_map.rb +43 -0
  30. data/lib/whisper/version.rb +3 -0
  31. data/share/whisper/comment-email.rtxt +36 -0
  32. data/share/whisper/config.yaml +53 -0
  33. data/share/whisper/entry-email.rtxt +35 -0
  34. data/share/whisper/entry.rhtml +88 -0
  35. data/share/whisper/entry.rtxt +19 -0
  36. data/share/whisper/formatter.rb +52 -0
  37. data/share/whisper/helper.rb +147 -0
  38. data/share/whisper/list.rhtml +51 -0
  39. data/share/whisper/list.rrss +9 -0
  40. data/share/whisper/list.rtxt +25 -0
  41. data/share/whisper/master.rhtml +81 -0
  42. data/share/whisper/master.rrss +21 -0
  43. data/share/whisper/master.rtxt +5 -0
  44. data/share/whisper/mootools.js +282 -0
  45. data/share/whisper/rss-badge.png +0 -0
  46. data/share/whisper/spinner.gif +0 -0
  47. data/share/whisper/style.css +314 -0
  48. metadata +239 -0
data/README ADDED
@@ -0,0 +1,59 @@
1
+ Whisper is a lightweight, simple weblaaagh server. Whisper is around 1400
2
+ lines of code, requires no database, can serve over 1500 requests per
3
+ second, and can output HTML, RSS, and text.
4
+
5
+ Demo: http://all-thing.net
6
+ News: http://all-thing.net/label/whisper/
7
+ Homepage: http://masanjin.net/whisper/
8
+
9
+ Features:
10
+
11
+ - No RDBMS. Because storing your blog entries in a RDBMS is like driving to
12
+ work in the Space Shuttle.
13
+
14
+ - YAML+Textile, sitting on a disk. Blog posts and comments are stored on
15
+ disk in regular files, using a mix of YAML and Textile. This means you can
16
+ keep your content under version control, and you can edit it with whatever
17
+ editor you desire.
18
+
19
+ - Sits directly on top of Rack (or Thin). No intermediate layer to slow
20
+ things down. For benchmarks, see http://all-thing.net/whisper-benchmarks.
21
+
22
+ - Lazy cached dependency graph: every bit of content is cached, built
23
+ lazily, and a part of a big dependency graph. That means almost every
24
+ request is served directly from memory, and making a change, like adding
25
+ or updating an entry, forces a regeneration of only those bits that
26
+ require it. Infrequently-requested bits of content eventually expire.
27
+
28
+ - Markup enhancements: Whisper has extra processing on top of Textile to
29
+ syntax highlight ruby code, and turn LaTeX math expressions are turned
30
+ into MathML (via RiTeX). Write pretty Ruby code and pretty math without
31
+ any extra effort.
32
+
33
+ - Fully threaded comments. Why would you not have this?
34
+
35
+ - Blog post as mailing list. Comments can be made by entering your email
36
+ address, and replying to the resulting email. You can choose to have
37
+ future replies emailed to you, and replying to them automatically adds a
38
+ comment. This allows you to quote, thread, and generally have a
39
+ reasonable discussion, which is what email is good at, and what typing
40
+ shit into little text areas on your web browser is not.
41
+
42
+ - Multiformat support. In addition to HTML and RSS output, there’s a plain
43
+ text mode for the hard-core.
44
+
45
+ - Pagination, labels, per-label and per-author indices, etc.
46
+
47
+ Caveats:
48
+
49
+ Whisper currently only supports comments via email. Textboxes are for the
50
+ birds.
51
+
52
+ Usage:
53
+
54
+ 1. gem install whisperblog
55
+ 2. whisper-init <blog directory>
56
+ 3. Follow the instructions!
57
+
58
+ To run whisper in production mode (probably what you want for serving your
59
+ blog), use --production.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rack'
5
+ require 'trollop'
6
+ require 'yaml'
7
+
8
+ require "whisper"
9
+
10
+ opts = Trollop.options do
11
+ text "Whisper v#{Whisper::VERSION}"
12
+
13
+ opt :whisper_dir, "Whisper root directory", :default => Dir.pwd
14
+ opt :port, "Port to run on (default: 80 in production mode, 9292 in development mode)", :short => 'p', :type => Integer
15
+ opt :socket, "Socket filename to run on instead of a TCP port (requires Thin)", :type => String
16
+ opt :development, "Development mode", :default => true, :short => :none
17
+ opt :production, "Production mode", :default => false, :short => :none
18
+ opt :log_level, "Logging level (default: 'info' in production mode, 'debug' in development mode)", :type => String
19
+ opt :log_file, "Log file ('-' for stdout)", :default => "-"
20
+ opt :console, "Run an IRB session instead of a web server", :default => false, :short => :none
21
+ opt :no_email, "Don't process incoming email", :default => false, :short => :none
22
+ conflicts :development, :production
23
+ conflicts :port, :socket
24
+ end
25
+
26
+ Trollop.die :log_level, "must be one of 'warn', 'info' or 'debug'" if opts[:log_level] unless %w(warn info debug).member? opts[:log_level]
27
+
28
+ if opts[:production]
29
+ opts[:development] = false
30
+ opts[:log_level] ||= "info"
31
+ else
32
+ opts[:development] = true
33
+ opts[:log_level] ||= "debug"
34
+ end
35
+
36
+ log_stream = opts[:log_file] == "-" ? $stdout : File.open(opts[:log_file], "w")
37
+ logger = Whisper::Logger.new log_stream, opts[:log_level]
38
+
39
+ config_fn = File.join opts[:whisper_dir], "config.yaml"
40
+ config = begin
41
+ Whisper::Config.new config_fn, opts[:whisper_dir], opts[:development] ? :development : :production
42
+ rescue SystemCallError => e
43
+ Trollop::die "Can't load configuration file #{config_fn}. Do you need to specify --whisper-dir, or run whisper-init?"
44
+ rescue Whisper::Config::Error => e
45
+ Trollop::die "Error loading configuration file #{config_fn}: #{e.message}"
46
+ end
47
+
48
+ if opts[:socket]
49
+ Trollop::die :socket, "cannot be specified if rack_handler configuration is not 'Thin'" unless config.rack_handler == "Thin"
50
+ end
51
+
52
+ whisper = Whisper::init config
53
+ server = Whisper::Server.new whisper[:blog], config.mode, config.static_dir, config.x_accel_redirect
54
+
55
+ if opts[:console]
56
+ @whisper = whisper
57
+ @server = server
58
+
59
+ require 'irb'
60
+ IRB.start __FILE__
61
+ exit
62
+ end
63
+
64
+ app = if config.mode == :development
65
+ Thread.abort_on_exception = true
66
+ Whisper::Dependency.min_refresh_interval = 1
67
+ Whisper::TimedMap.expiration_interval = 1
68
+ Whisper::DirScanner.min_scan_interval = 1
69
+ Whisper::CachedFile.min_scan_interval = 1
70
+
71
+ Rack::Builder.new do
72
+ use Rack::CommonLogger, $stdout
73
+ use Rack::ShowExceptions
74
+ use Rack::Lint
75
+ run server
76
+ end.to_app
77
+ else
78
+ whisper[:sender].start!
79
+ whisper[:receiver].start! unless opts[:no_email]
80
+
81
+ server
82
+ end
83
+
84
+ handler = Rack::Handler::get config.rack_handler.downcase
85
+ run_opts = if opts[:socket]
86
+ { :Host => File.expand_path(opts[:socket]) }
87
+ else
88
+ { :Port => (opts[:port] || config.use_this_port) }
89
+ end
90
+
91
+ handler.run app, run_opts
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'trollop'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+ require "whisper"
8
+
9
+ def die s; puts s; exit end
10
+ def mkdir d
11
+ FileUtils.mkdir d
12
+ puts "+ created: #{d}"
13
+ end
14
+ def copy f, t
15
+ FileUtils.cp f, t
16
+ puts "+ created: #{t}/#{File.basename(f)}"
17
+ end
18
+
19
+ dir = ARGV.shift or die("Expecting one argument: the directory to create")
20
+ mkdir dir
21
+
22
+ %w(templates static comments drafts posts authors).each { |d| mkdir File.join(dir, d) }
23
+ share_dir = File.join File.dirname(__FILE__), "..", "share", "whisper"
24
+
25
+ %w(config.yaml helper.rb formatter.rb).each { |f| copy File.join(share_dir, f), dir }
26
+ %w(style.css mootools.js rss-badge.png spinner.gif).each { |f| copy File.join(share_dir, f), File.join(dir, "static") }
27
+
28
+ template_dir = File.join(dir, "templates")
29
+ %w(rhtml rtxt rrss).map { |ext| Dir[File.join(share_dir, "*.#{ext}")] }.flatten.each do |f|
30
+ copy f, template_dir
31
+ end
32
+
33
+ puts <<EOS
34
+ Directory setup is complete. Remaining steps:
35
+ 1. Edit #{File.join dir, "config.yaml"} to configure your weblaaagh.
36
+ 2. Run "whisper-post -w #{dir}" to add a new post.
37
+ 3. Run "whisper -w #{dir}" and see blog at http://localhost:9292.
38
+ 4. To customize, edit #{template_dir}/*, helper.rb and formatter.rb.
39
+ 5. Have fun.
40
+ EOS
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'trollop'
5
+ require 'yaml'
6
+ require 'readline'
7
+ require 'fileutils'
8
+
9
+ require "whisper"
10
+
11
+ def ask q
12
+ ans = Readline.readline q
13
+ Readline::HISTORY.push ans
14
+ ans
15
+ end
16
+
17
+ def ask_complete q, comps
18
+ Readline.completion_append_character = nil
19
+ Readline.completion_proc = lambda { |prefix| comps.select { |c| c[0 ... prefix.length] == prefix } }
20
+ ans = Readline.readline q
21
+ Readline::HISTORY.push ans
22
+ ans
23
+ end
24
+
25
+ opts = Trollop.options do
26
+ text "Whisper v#{Whisper::VERSION}"
27
+ opt :whisper_dir, "Whisper root directory", :default => "."
28
+ end
29
+
30
+ config_fn = File.join opts[:whisper_dir], "config.yaml"
31
+ begin
32
+ config = Whisper::Config.new config_fn, opts[:whisper_dir]
33
+ rescue SystemCallError
34
+ Trollop::die "can't read config file #{config_fn}. Do you need --whisper-dir?"
35
+ end
36
+
37
+ tags = []
38
+ authors = []
39
+ ids = []
40
+ Dir[File.join(config.post_dir, "*#{Whisper::ENTRY_METADATA_EXTENSION}")].each do |fn|
41
+ yaml = YAML.load_file(fn)
42
+ tags += yaml[:labels] if yaml[:labels]
43
+ authors << yaml[:author]
44
+ ids << yaml[:id]
45
+ end
46
+ cur_tags = tags.flatten.uniq
47
+ cur_authors = authors.flatten.uniq
48
+
49
+ title = ask "Post title: "
50
+ default_id = title.downcase.gsub(/[^\w0-9\.]+/, "-").gsub(/\-+$/, "")
51
+ id = ask_complete "Post id (will be used for url) [#{default_id}]: ", ids
52
+ id = default_id if id.empty?
53
+ tags = ask_complete("Tags (space-separated, tab for completion): ", cur_tags).split(/\s+/)
54
+ author = ask_complete "Author email address (in \"Name <address@domain.tld>\" format, tab for completion): ", cur_authors until author && author.email_name && author.email_address
55
+
56
+ fn = id
57
+ real_fn = while true
58
+ real_fn = fn + Whisper::ENTRY_METADATA_EXTENSION
59
+ break real_fn unless File.exist? real_fn
60
+ fn += "-again"
61
+ end
62
+
63
+ yaml_fn = File.join config.draft_dir, real_fn
64
+ puts "Writing #{yaml_fn}..."
65
+ File.open(yaml_fn, "w") do |f|
66
+ f.print({ :author => author, :published => Time.now, :updated => Time.now,
67
+ :id => id, :title => title, :labels => tags }.to_yaml)
68
+ end
69
+
70
+ textile_fn = File.join config.draft_dir, (fn + Whisper::ENTRY_CONTENT_EXTENSION)
71
+ editor = ENV["EDITOR"] || "/bin/vi"
72
+ puts "Editing #{textile_fn} with #{editor}..."
73
+ system "#{editor} #{textile_fn}"
74
+
75
+ puts "Done!"
76
+
77
+ if Readline.readline("Edit metadata (y/N)? ") =~ /^[Yy]$/
78
+ puts "Editing #{yaml_fn} with #{editor}..."
79
+ system "#{editor} #{yaml_fn}"
80
+ end
81
+
82
+ if Readline.readline("Publish (Y/n)? ") =~ /^[Yy]?$/
83
+ FileUtils.mv textile_fn, config.post_dir
84
+ FileUtils.mv yaml_fn, config.post_dir
85
+ puts "Published to #{config.post_dir}!"
86
+ else
87
+ puts "Saved as a draft."
88
+ end
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rack'
5
+ require 'trollop'
6
+ require 'yaml'
7
+
8
+ require "whisper"
9
+
10
+ opts = Trollop.options do
11
+ text "Whisper v#{Whisper::VERSION}"
12
+
13
+ opt :whisper_dir, "Whisper root directory", :default => Dir.pwd
14
+ opt :log_level, "Set log level (default: 'info')", :type => String, :default => "debug"
15
+ opt :send, "In addition to writing comments, resend email", :default => false
16
+ opt :num, "Just process this many emails", :type => Integer
17
+ opt :start, "Offset to start at, if not the current mbox offset", :type => Integer
18
+ end
19
+
20
+ Trollop.die :log_level, "must be one of 'warn', 'info' or 'debug'" if opts[:log_level] unless %w(warn info debug).member? opts[:log_level]
21
+ Trollop.die "expecting no arguments" unless ARGV.empty?
22
+
23
+ logger = Whisper::Logger.new $stdout, opts[:log_level]
24
+
25
+ config_fn = File.join opts[:whisper_dir], "config.yaml"
26
+ config = begin
27
+ Whisper::Config.new config_fn, opts[:whisper_dir], opts[:development] ? :development : :production
28
+ rescue SystemCallError => e
29
+ Trollop::die "Can't load configuration file #{config_fn}. Do you need to specify --whisper-dir, or run whisper-init?"
30
+ rescue Whisper::Config::Error => e
31
+ Trollop::die "Error loading configuration file #{config_fn}: #{e.message}"
32
+ end
33
+
34
+ whisper = Whisper::init config
35
+ receiver, sender = whisper[:receiver], whisper[:sender]
36
+ receiver.offset = opts[:start] if opts[:start]
37
+
38
+ num = 0
39
+ until receiver.done?
40
+ break if opts[:num] && num >= opts[:num]
41
+ receiver.step
42
+ num += 1
43
+ end
44
+
45
+ puts "Processed #{num} messages."
46
+
47
+ if opts[:send]
48
+ puts "Have #{sender.q_size} replies to send..."
49
+ sender.process until sender.done?
50
+ else
51
+ puts "NOT sending #{sender.q_size} scheduled reply emails."
52
+ end
53
+
54
+ puts "Done!"
@@ -0,0 +1,52 @@
1
+ module Whisper
2
+ ENTRY_METADATA_EXTENSION = ".yaml"
3
+ ENTRY_CONTENT_EXTENSION = ".textile"
4
+
5
+ def init config
6
+ ## load the formatters
7
+ formatter_fn = File.join(config.formatter_dir, "formatter.rb")
8
+ begin
9
+ load formatter_fn
10
+ rescue SystemCallError
11
+ Logger.instance.warn "no formatter plugins file #{formatter_fn} found"
12
+ end
13
+
14
+ entryset = Whisper::EntrySet.new config.post_dir
15
+ commentset = Whisper::CommentSet.new config.comment_dir
16
+ router = Whisper::Router.new config.root
17
+ authors = Whisper::AuthorTracker.new config.author_dir
18
+ blog = Whisper::Blog.new config, entryset, commentset, router, authors
19
+ sender = Whisper::EmailSender.new entryset, commentset, router, authors, blog.template_for("entry-email", "txt"), blog.template_for("comment-email", "txt"), config
20
+ receiver = Whisper::EmailReceiver.new entryset, commentset, sender, authors, config.comment_mbox, config.mbox_offset_filename, config.comment_dir
21
+
22
+ blog.install_default_routes! sender
23
+
24
+ { entryset: entryset, commentset: commentset, router: router, authors: authors,
25
+ blog: blog, sender: sender, receiver: receiver
26
+ }
27
+ end
28
+
29
+ module_function :init
30
+ end
31
+
32
+ require "whisper/version.rb"
33
+ require "whisper/common.rb"
34
+ require "whisper/dir_scanner.rb"
35
+ require "whisper/text.rb"
36
+ require "whisper/entry.rb"
37
+ require "whisper/comment.rb"
38
+ require "whisper/config.rb"
39
+ require "whisper/cached_file.rb"
40
+ require "whisper/page.rb"
41
+ require "whisper/server.rb"
42
+ require "whisper/dir_set.rb"
43
+ require "whisper/entry_set.rb"
44
+ require "whisper/comment_set.rb"
45
+ require "whisper/timed_map.rb"
46
+ require "whisper/handler.rb"
47
+ require "whisper/router.rb"
48
+ require "whisper/blog.rb"
49
+ require "whisper/mbox.rb"
50
+ require "whisper/email_receiver.rb"
51
+ require "whisper/email_sender.rb"
52
+ require "whisper/author_tracker.rb"
@@ -0,0 +1,46 @@
1
+ require "whisper/common"
2
+ require 'thread'
3
+
4
+ module Whisper
5
+ class AuthorTracker
6
+ include Loggy
7
+
8
+ def initialize dir
9
+ @dir = dir
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def [] name
14
+ @mutex.synchronize do
15
+ fn = fn_for name
16
+ begin
17
+ YAML.load_file fn
18
+ rescue SystemCallError => e
19
+ {}
20
+ end
21
+ end
22
+ end
23
+
24
+ def []= name, info
25
+ unless info.empty?
26
+ @mutex.synchronize do
27
+ fn = fn_for name
28
+ old_info = YAML.load_file(fn) rescue {}
29
+ info = old_info.merge info
30
+ debug "writing #{info.inspect} to #{fn}..."
31
+ begin
32
+ File.open(fn, "w") { |f| f.print info.to_yaml }
33
+ rescue SystemCallError => e
34
+ warn "can't write #{fn}: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+ info
39
+ end
40
+
41
+ private
42
+
43
+ def fn_for name; File.join(@dir, name.gsub(/[\/\.]/, "-")) + ".yaml" end
44
+
45
+ end
46
+ end
@@ -0,0 +1,156 @@
1
+ require "whisper/cached_file"
2
+ require "whisper/page"
3
+ require "whisper/common"
4
+
5
+ module Whisper
6
+
7
+ class Blog
8
+ include Loggy
9
+
10
+ MAX_RECENT_COMMENTS = 30
11
+ attr_accessor :entryset, :helper
12
+
13
+ def initialize config, entryset, commentset, router, authors
14
+ @config = config
15
+ @entryset = entryset
16
+ @commentset = commentset
17
+ @router = router
18
+ @authors = authors
19
+ @helper = CachedFile.new File.join(config.helper_dir, "helper.rb")
20
+ @templates = {} # we'll create these lazily
21
+ @paginations = {}
22
+ end
23
+
24
+ def title; @config.title end
25
+ def tagline; @config.tagline end
26
+
27
+ def get_content_for path, params, request_method; @router.get_content_for path, params, request_method end
28
+ def template_for name, format
29
+ @templates[[name, format]] ||= CachedFile.new File.join(@config.template_dir, "#{name}.r#{format}")
30
+ end
31
+
32
+ ## we're basically doing this by hand, but we could also make a separate
33
+ ## paginator object and fit it into the dependency model.
34
+ def paginate id, timestamp, things
35
+ time, pages = @paginations[id]
36
+ if time.nil? || time < timestamp
37
+ pages = things.paginate @config.page_size
38
+ pages[0] ||= []
39
+ debug "repaginated #{id.inspect}: turned #{things.size} things => #{pages.size} pages"
40
+ time = Time.now
41
+ @paginations[id] = [time, pages]
42
+ end
43
+ pages
44
+ end
45
+
46
+ def install_default_routes! emailer=nil
47
+ common_opts = { :config => @config, :router => @router, :helper => @helper, :extra_deps => [@entryset, @commentset], :entryset => @entryset }
48
+
49
+ @router.add_handler :index, :get, "/index/:page:format" do |route, page, format, params|
50
+ debug "index handler called for page #{page.inspect} format #{format.inspect}"
51
+
52
+ Page.new common_opts.merge({ :format => format, :url_vars => { :route => route },
53
+ :master_template => template_for("master", format),
54
+ :template => template_for("list", format) }) do
55
+
56
+ pages = paginate :entries_by_page, @entryset.timestamp, @entryset.entries
57
+ entries = pages[page] or raise InvalidPageError
58
+ comments = comments_for_entries entries
59
+ { :entries => entries.zip(comments), :pages => pages.size, :page => page,
60
+ :recent_comments => recent_comments_and_entries, :route => route
61
+ }
62
+ end
63
+ end
64
+
65
+ @router.add_handler :label, :get, "/label/+label/:page:format" do |route, label, page, format, params|
66
+ debug "label handler called for label #{label.inspect} page #{page.inspect} format #{format.inspect}"
67
+
68
+ Page.new common_opts.merge({:format => format, :url_vars => { :route => route, :label => label },
69
+ :master_template => template_for("master", format),
70
+ :template => template_for("list", format) }) do
71
+
72
+ raw_entries = @entryset.entries_by_label[label] or raise InvalidPageError
73
+ pages = paginate [:entries_by_label, label], @entryset.timestamp, raw_entries
74
+ entries = pages[page] or raise InvalidPageError
75
+ comments = comments_for_entries entries
76
+ { :entries => entries.zip(comments), :pages => pages.size, :page => page,
77
+ :recent_comments => recent_comments_and_entries, :route => route, :label => label
78
+ }
79
+ end
80
+ end
81
+
82
+ @router.add_handler :author, :get, "/by/+author/:page:format" do |route, author, page, format, params|
83
+ debug "author handler called for author #{author.inspect} page #{page.inspect} format #{format.inspect}"
84
+
85
+ Page.new common_opts.merge({:format => format, :url_vars => { :route => route, :author => author },
86
+ :master_template => template_for("master", format),
87
+ :template => template_for("list", format) }) do
88
+
89
+ pages = paginate [:entries_by_author, author], @entryset.timestamp, @entryset.entries_by_author[author]
90
+ entries = pages[page] or raise InvalidPageError
91
+ comments = comments_for_entries entries
92
+ { :entries => entries.zip(comments), :pages => pages.size, :page => page,
93
+ :recent_comments => recent_comments_and_entries, :route => route, :author => author
94
+ }
95
+ end
96
+ end
97
+
98
+ ## allow formats on /. why not.
99
+ @router.add_handler :root, :get, "/:format" do |route, format, params|
100
+ debug "root handler called with format #{format}"
101
+ Page.new common_opts.merge({ :format => format, :url_vars => { :route => :index },
102
+ :template => template_for("list", format),
103
+ :master_template => template_for("master", format) }) do
104
+
105
+ pages = paginate :entries_by_page, @entryset.timestamp, @entryset.entries
106
+ entries = pages[0]
107
+ comments = comments_for_entries entries
108
+ { :entries => entries.zip(comments), :pages => pages.size, :page => 0,
109
+ :recent_comments => recent_comments_and_entries, :route => route
110
+ }
111
+ end
112
+ end
113
+
114
+ if emailer
115
+ @router.add_handler :comment, :post, "/comment/+id" do |route, id, params|
116
+ debug "request comment handler called for id #{id} with params #{params.inspect}"
117
+ begin
118
+ emailer.handle_send_request id, params if emailer
119
+ ["email sent!", "text/plain"]
120
+ rescue EmailSender::Error => e
121
+ ["error: #{e.message}", "text/plain"]
122
+ end
123
+ end
124
+ end
125
+
126
+ ## individual entry handler is the most generic (entry ids can be close to
127
+ ## anything), so we install it last.
128
+ @router.add_handler :entry, :get, "/+id:format" do |route, id, format, params|
129
+ debug "entry handler called for id #{id.inspect} format #{format.inspect}"
130
+
131
+ Page.new common_opts.merge({ :format => format, :url_vars => { :route => route, :id => id },
132
+ :template => template_for("entry", format),
133
+ :master_template => template_for("master", format)
134
+ }) do
135
+
136
+ raise InvalidPageError unless (entry = @entryset.entries_by_id[id])
137
+ { :entry => entry, :comments => (@commentset.comment_tree_by_entry_id[entry.id] || {}),
138
+ :recent_comments => recent_comments_and_entries, :route => route,
139
+ :author_info => @authors
140
+ }
141
+ end
142
+ end
143
+ end
144
+
145
+ def recent_comments_and_entries
146
+ comments = @commentset.comments[0 ... MAX_RECENT_COMMENTS]
147
+ entries_per_comment = @entryset.entries_by_id.values_at(*comments.map { |c| c.entry_id })
148
+ comments.zip entries_per_comment
149
+ end
150
+
151
+ def comments_for_entries entries
152
+ @commentset.comments_by_entry_id.values_at(*entries.map { |e| e.id }).map { |x| x || [] }
153
+ end
154
+ end
155
+
156
+ end