whisperblog 0.6

Sign up to get free protection for your applications and to get access to all the features.
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