Permalink
Browse files

content filtering / add :comment_filters option / rdiscount support

Filters are just helper methods that take a single text argument
and apply some transformation. The "filter" method can be used to
chain multiple filters together:

    filter text, :markdown, :smartify, :sanitize

This is equivalent to:

    sanitize(smartify(markdown(text)))

The new system is now being used for comments, entry bodies, and
summaries.

The :comment_filters option can be set to an array of filters to apply
to comments.

While here, added support for rdiscount if installed; falls back to
BlueCloth.
  • Loading branch information...
rtomayko committed Jun 7, 2008
1 parent 6d6f0e0 commit 0bc3587c6b81e9d57d941d9d54607a96024cb03e
Showing with 132 additions and 76 deletions.
  1. +3 −0 lib/wink.rb
  2. +26 −0 lib/wink/markdown.rb
  3. +53 −25 lib/wink/web.rb
  4. +14 −16 views/comments.haml
  5. +14 −16 views/entry.haml
  6. +7 −7 views/index.haml
  7. +15 −12 wink.conf.example
View
@@ -60,6 +60,9 @@
# Start date for archives + copyright notice.
set :begin_date, Date.today.year
+ # List of filters to apply to comments.
+ set :comment_filters, [:markdown, :sanitize]
+
end
View
@@ -0,0 +1,26 @@
+module Wink
+
+ require 'rdiscount'
+ Markdown = RDiscount
+
+rescue LoadError => boom
+
+ warn 'Could not require rdiscount, using BlueCloth for Markdown processing.'
+
+ require 'bluecloth'
+ require 'rubypants'
+
+ class Markdown
+ def initialize(text, *options)
+ @text = text
+ @options = options
+ @smart = true if options.include?(:smart)
+ end
+ def to_html(ignored=nil)
+ text = BlueCloth.new(@text).to_html
+ text = RubyPants.new(text).to_html if @smart
+ text
+ end
+ end
+
+end
View
@@ -1,48 +1,70 @@
require 'sinatra'
require 'haml'
-require 'bluecloth'
-require 'rubypants'
require 'html5/html5parser'
require 'html5/sanitizer'
require 'wink'
+require 'wink/markdown'
require 'wink/models'
-
-helpers do
+module Wink::Helpers
include Rack::Utils
+ alias :h :escape_html
- def h(string)
- escape_html(string)
+ # Sanitize HTML - removes potentially dangerous markup like <script> and
+ # <object> tags.
+ def sanitize(html)
+ HTML5::HTMLParser.
+ parse_fragment(html, :tokenizer => HTML5::HTMLSanitizer, :encoding => 'utf-8').
+ to_s
end
- def markdown_filter(text)
- html = BlueCloth.new(text || '').to_html
- html.chomp!
- html.chomp!('<hr/>')
- html.chomp!
- RubyPants.new(html).to_html
- rescue => boom
- "<p><strong>Boom!</strong></p><pre>#{h(boom.to_s)}</pre>"
+ # Convert text to HTML using Markdown (includes text smartification). Uses
+ # RDiscount if available and falls back to BlueCloth.
+ def markdown(text)
+ return '' if text.nil? || text.empty?
+ Wink::Markdown.new(text, :smart).to_html
+ end
+
+ # Make text "smart" by converting dumb puncuation characters to their
+ # high-class equivalents.
+ def smartify(text)
+ return '' if text.nil? || text.empty?
+ RubyPants.new(text).to_html
end
- def text_filter(text)
- "<p>#{escape_html(text || '')}</p>"
+ # Apply a list of content filters to text. Calls each helper method in
+ # the order specified with the result of the previous operation. The following
+ # are equivalent:
+ #
+ # filter "Hello, World.", :markdown, :sanitize
+ # sanitize(markdown("Hello, World."))
+ #
+ def filter(text, *filters)
+ filters.inject(text) do |text,method_name|
+ send(method_name, text)
+ end
+ rescue => boom
+ "<p><strong>Boom!</strong></p><pre>#{escape_html(boom.to_s)}</pre>"
end
- def html_filter(text)
+ def html(text)
text || ''
end
- def content_filter(text, filter=:markdown)
- send("#{filter}_filter", text)
+ # The comment's formatted body.
+ def comment_body(comment=@comment)
+ filter(comment.body, *Wink.comment_filters)
end
- # Sanitize HTML using html5lib.
- def sanitize(html)
- HTML5::HTMLParser.
- parse_fragment(html, :tokenizer => HTML5::HTMLSanitizer, :encoding => 'utf-8').
- to_s
+ # The entry's formatted body.
+ def entry_body(entry=@entry)
+ filter(entry.body, entry.filter)
+ end
+
+ # The entry's formatted summary.
+ def entry_summary(entry=@entry)
+ filter(entry.summary, :markdown)
end
# Convert hash to HTML attribute string.
@@ -152,8 +174,14 @@ def selectbox(name, value, options)
def wink
Wink
end
+
end
+
+# Bring Wink::Helpers into Sinatra
+helpers { include Wink::Helpers }
+
+
# Resources =================================================================
get '/' do
@@ -307,7 +335,7 @@ def wink
get '/comments/:id' do
comment = Comment[params[:id].to_i]
raise Sinatra::NotFound if comment.nil?
- content_filter(comment.body, :markdown)
+ comment_body(comment)
end
post '/writings/:slug/comment' do
View
@@ -10,32 +10,30 @@
#comments
%ol.comments
- - @comments.each do |c|
- %li{ :id => c.id, :class => "comment #{c.spam? ? 'spam' : 'ham'} container" }
+ - @comments.each do |@comment|
+ %li{ :id => @comment.id, :class => "comment #{@comment.spam? ? 'spam' : 'ham'} container" }
%h3
- -if c.author_link?
- %a.commentor{ :href => h(c.author_link) }= h(c.author)
+ -if @comment.author_link?
+ %a.commentor{ :href => h(@comment.author_link) }= h(@comment.author)
-else
- %span.commentor= h(c.author)
+ %span.commentor= h(@comment.author)
&nbsp;on&nbsp;
- = entry_ref(c.entry)
+ = entry_ref(@comment.entry)
%div.body
- ~ sanitize(content_filter(c.body, :markdown))
+ ~ comment_body
-if admin?
- %form{:action=>"/comments/#{c.id}",:style=>'display:none'}
+ %form{:action=>"/comments/#{@comment.id}",:style=>'display:none'}
%div
- ~ textarea(:body, c.body, :rows => 5)
+ ~ textarea(:body, @comment.body, :rows => 5)
%input{:type=>'submit',:value=>'Save'}
%p.comment-meta
- = c.created_at.strftime("%a, %b %d, %Y")
+ = @comment.created_at.strftime("%a, %b %d, %Y")
at
- = c.created_at.strftime("%I:%M %p")
+ = @comment.created_at.strftime("%I:%M %p")
-if admin?
- %a{:href => "/comments/#{c.id}", :class => 'edit' }Edit
+ %a{:href => "/comments/#{@comment.id}", :class => 'edit' }Edit
|
- %a{:href => "/comments/#{c.id}", :rel => 'delete' }Delete
+ %a{:href => "/comments/#{@comment.id}", :rel => 'delete' }Delete
|
- %a{:href => entry_url(c.entry) + "#comment-#{c.id}"}#
-
--# vim: ts=2 sw=2 sts=0 expandtab tw=120
+ %a{:href => entry_url(@comment.entry) + "#comment-#{@comment.id}"}#
View
@@ -11,7 +11,7 @@
</a>
#body
- ~ content_filter(@entry.body, @entry.filter)
+ ~ entry_body
#appendix
%p
@@ -31,31 +31,31 @@
-if @comments && @comments.any?
%h2 Discuss
%ol.comments
- - @comments.each do |c|
- %li.comment.container{:id=>"comment-#{c.id}"}
+ - @comments.each do |@comment|
+ %li.comment.container{:id=>"comment-#{@comment.id}"}
.body
- ~ sanitize(content_filter(c.body, :markdown))
+ ~ comment_body
-if admin?
- %form{:action=>"/comments/#{c.id}",:style=>'display:none'}
+ %form{:action=>"/comments/#{@comment.id}",:style=>'display:none'}
%div
- ~ textarea :body, c.body, :rows => 5
+ ~ textarea :body, @comment.body, :rows => 5
<input type='submit' value='Save'>
%p.comment-meta
&mdash;
- -if c.author_link?
- %a.commentor{:href => h(c.author_link)}= h(c.author)
+ -if @comment.author_link?
+ %a.commentor{:href => h(@comment.author_link)}= h(@comment.author)
-else
- %span.commentor= h(c.author)
+ %span.commentor= h(@comment.author)
on
- = c.created_at.strftime("%A, %B %d, %Y")
+ = @comment.created_at.strftime("%A, %B %d, %Y")
at
- = c.created_at.strftime("%I:%M %p")
+ = @comment.created_at.strftime("%I:%M %p")
-if admin?
- %a{:href => "/comments/#{c.id}", :class => 'edit' }Edit
+ %a{:href => "/comments/#{@comment.id}", :class => 'edit' }Edit
|
- %a{:href => "/comments/#{c.id}", :rel => 'delete' }Delete
+ %a{:href => "/comments/#{@comment.id}", :rel => 'delete' }Delete
|
- %a{:title=>'Permanent link to this comment', :href=>"#comment-#{c.id}"}#
+ %a{:title=>'Permanent link to this comment', :href=>"#comment-#{@comment.id}"}#
%h2 Leave a comment
%form.leave{ :action => "#{request.url}/comment", :method => "post" }
@@ -73,5 +73,3 @@
%br
%textarea{:name => 'body', :id => 'comment_body', :style => 'width:100%', :rows => 8 }= ''
%input{:type => 'submit', :value => 'Leave Comment'}
-
--# vim: tw=120 ts=2 sw=2 sts=0 expandtab
View
@@ -1,13 +1,13 @@
#body.index
- - @entries.each do |entry|
- %div{ :class => "entry #{entry.class.name.downcase}" }
+ - @entries.each do |@entry|
+ %div{ :class => "entry #{@entry.class.name.downcase}" }
%h3
- = entry_ref(entry)
+ = entry_ref(@entry)
%p.meta.caps
- = entry.created_at.strftime('%A, %B %d, %Y at %I:%M %p')
+ = @entry.created_at.strftime('%A, %B %d, %Y at %I:%M %p')
\/ By
= wink.author
- -if entry.domain
- = "/ #{entry.domain}"
- ~ content_filter(entry.summary, :markdown)
+ -if @entry.domain
+ = "/ #{@entry.domain}"
+ ~ entry_summary
View
@@ -1,6 +1,15 @@
-# Wink Configuration
+# Database Configuration ====================================================
+
+Database.configure \
+ :adapter => 'mysql',
+ :host => 'localhost',
+ :username => 'root',
+ :password => '',
+ :database => 'wink_development'
+# Wink Configuration ========================================================
+
# The site's root URL. Note: the trailing slash should be
# omitted when setting this option.
set :url, 'http://localhost:4567'
@@ -24,23 +33,17 @@ set :linkings, 'Linkings'
## Start date for archives + copyright notice
set :begin_date, 2008
+## List of filters to apply to comments (make sure the :sanitize filter
+## is included).
+#set :comment_filters, [:markdown, :sanitize]
+
## Where to write log messages (defaults to STDERR)
#set :log_stream, File.open('development.log', 'ab')
## This site's Akismet key (should pair with the url option).
-set :akismet_key, 'd3cafbad'
+#set :akismet_key, 'd3cafbad'
## The del.icio.us username/password used for bookmark synchronization.
#set :delicious, %w[username password]
-# Database Configuration
-
-Database.configure \
- :adapter => 'mysql',
- :host => 'localhost',
- :username => 'root',
- :password => '',
- :database => 'wink_development'
-
-
# vim: ft=ruby

0 comments on commit 0bc3587

Please sign in to comment.