Treat all newly entered blocks of text as Markdown
authorTom Hughes <tom@compton.nu>
Wed, 15 Feb 2012 00:48:52 +0000 (00:48 +0000)
committerTom Hughes <tom@compton.nu>
Sat, 17 Mar 2012 16:36:55 +0000 (16:36 +0000)
21 files changed:
Gemfile
Gemfile.lock
app/helpers/application_helper.rb
app/models/diary_comment.rb
app/models/diary_entry.rb
app/models/message.rb
app/models/user.rb
app/models/user_block.rb
app/views/diary_entry/_diary_comment.html.erb
app/views/diary_entry/_diary_entry.html.erb
app/views/diary_entry/comments.html.erb
app/views/diary_entry/rss.rss.builder
app/views/message/read.html.erb
app/views/notifier/diary_comment_notification.text.erb
app/views/notifier/message_notification.html.erb
app/views/notifier/message_notification.text.erb
app/views/user/_user.html.erb
app/views/user/view.html.erb
app/views/user_blocks/show.html.erb
db/migrate/20120214210114_add_text_format.rb [new file with mode: 0644]
lib/rich_text.rb [new file with mode: 0644]

diff --git a/Gemfile b/Gemfile
index c1bb2f5..e283179 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,9 @@ gem 'http_accept_language', '>= 1.0.2'
 gem 'paperclip', '~> 2.0'
 gem 'deadlock_retry', '>= 1.2.0'
 
+# Markdown formatting support
+gem 'redcarpet'
+
 # Character conversion support for ruby 1.8
 gem 'iconv', :platforms => :ruby_18
 
index 4709cd1..0dcb5cc 100644 (file)
@@ -120,6 +120,7 @@ GEM
     rake (0.9.2.2)
     rdoc (3.12)
       json (~> 1.4)
+    redcarpet (2.1.0)
     rinku (1.5.1)
     ruby-openid (2.1.8)
     sanitize (2.0.3)
@@ -168,6 +169,7 @@ DEPENDENCIES
   pg
   rails (= 3.2.2)
   rails-i18n (>= 0.5.1)
+  redcarpet
   rinku (>= 1.2.2)
   sanitize
   sass-rails (~> 3.2.3)
index 4aec9f5..7a72932 100644 (file)
@@ -1,14 +1,6 @@
 module ApplicationHelper
   require 'rexml/document'
 
-  def sanitize(text)
-    Sanitize.clean(text, Sanitize::Config::OSM).html_safe
-  end
-
-  def htmlize(text)
-    return linkify(sanitize(simple_format(text)))
-  end
-
   def linkify(text)
     if text.html_safe?
       Rinku.auto_link(text, :urls, tag_options(:rel => "nofollow")).html_safe
index b915e02..075d288 100644 (file)
@@ -7,6 +7,12 @@ class DiaryComment < ActiveRecord::Base
 
   attr_accessible :body
 
+  after_initialize :set_defaults
+
+  def body
+    RichText.new(read_attribute(:body_format), read_attribute(:body))
+  end
+
   def digest
     md5 = Digest::MD5.new
     md5 << diary_entry_id.to_s
@@ -15,4 +21,10 @@ class DiaryComment < ActiveRecord::Base
     md5 << body
     md5.hexdigest
   end
+
+private
+
+  def set_defaults
+    self.body_format = "markdown" unless self.attribute_present?(:body_format)
+  end
 end
index 1d83635..64a412d 100644 (file)
@@ -25,4 +25,16 @@ class DiaryEntry < ActiveRecord::Base
   validates_associated :language
 
   attr_accessible :title, :body, :language_code, :latitude, :longitude
+
+  after_initialize :set_defaults
+
+  def body
+    RichText.new(read_attribute(:body_format), read_attribute(:body))
+  end
+
+private
+
+  def set_defaults
+    self.body_format = "markdown" unless self.attribute_present?(:body_format)
+  end
 end
index 0b34160..feceec5 100644 (file)
@@ -11,6 +11,12 @@ class Message < ActiveRecord::Base
 
   attr_accessible :title, :body
 
+  after_initialize :set_defaults
+
+  def body
+    RichText.new(read_attribute(:body_format), read_attribute(:body))
+  end
+
   def digest
     md5 = Digest::MD5.new
     md5 << from_user_id.to_s
@@ -20,4 +26,10 @@ class Message < ActiveRecord::Base
     md5 << body
     md5.hexdigest
   end
+
+private
+
+  def set_defaults
+    self.body_format = "markdown" unless self.attribute_present?(:body_format)
+  end
 end
index 0c9e76d..3b55040 100644 (file)
@@ -44,7 +44,7 @@ class User < ActiveRecord::Base
   attr_accessible :display_name, :email, :email_confirmation, :openid_url,
                   :pass_crypt, :pass_crypt_confirmation, :consider_pd
 
-  after_initialize :set_creation_time
+  after_initialize :set_defaults
   before_save :encrypt_password
 
   has_attached_file :image, 
@@ -101,6 +101,10 @@ class User < ActiveRecord::Base
     return el1
   end
 
+  def description
+    RichText.new(read_attribute(:description_format), read_attribute(:description))
+  end
+
   def languages
     attribute_present?(:languages) ? read_attribute(:languages).split(/ *, */) : []
   end
@@ -220,8 +224,9 @@ class User < ActiveRecord::Base
 
 private
 
-  def set_creation_time
+  def set_defaults
     self.creation_time = Time.now.getutc unless self.attribute_present?(:creation_time)
+    self.description_format = "markdown" unless self.attribute_present?(:description_format)
   end
 
   def encrypt_password
index 7bf8f86..8821926 100644 (file)
@@ -5,8 +5,16 @@ class UserBlock < ActiveRecord::Base
   belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
   belongs_to :revoker, :class_name => "User", :foreign_key => :revoker_id
   
+  after_initialize :set_defaults
+
   PERIODS = USER_BLOCK_PERIODS
 
+  ##
+  # return a renderable version of the reason text.
+  def reason
+    RichText.new(read_attribute(:reason_format), read_attribute(:reason))
+  end
+
   ##
   # returns true if the block is currently active (i.e: the user can't
   # use the API).
@@ -25,7 +33,14 @@ class UserBlock < ActiveRecord::Base
     }, :without_protection => true)
   end
 
-  private
+private
+
+  ##
+  # set default values for new records.
+  def set_defaults
+    self.reason_format = "markdown" unless self.attribute_present?(:reason_format)
+  end
+
   ##
   # validate that only moderators are allowed to change the
   # block. this should be caught and dealt with in the controller,
index 05cb801..463c2ad 100644 (file)
@@ -1,6 +1,6 @@
 <%= user_thumbnail diary_comment.user %>
 <h4 id="comment<%= diary_comment.id %>"><%= raw(t('diary_entry.diary_comment.comment_from', :link_user => (link_to h(diary_comment.user.display_name), :controller => 'user', :action => 'view', :display_name => diary_comment.user.display_name), :comment_created_at => l(diary_comment.created_at, :format => :friendly))) %></h4>
-<%= htmlize(diary_comment.body) %>
+<%= diary_comment.body.to_html %>
 <%= if_administrator(:span) do %> 
   <%= link_to t('diary_entry.diary_comment.hide_link'), {:action => 'hidecomment', :display_name => diary_comment.diary_entry.user.display_name, :id => diary_comment.diary_entry.id, :comment => diary_comment.id}, {:confirm => t('diary_entry.diary_comment.confirm')} %>
 <% end %>
index 297f74e..21c3ef6 100644 (file)
@@ -1,7 +1,7 @@
 <b><%= link_to h(diary_entry.title), :action => 'view', :display_name => diary_entry.user.display_name, :id => diary_entry.id %></b><br />
 
 <div xml:lang="<%= diary_entry.language_code %>" lang="<%= diary_entry.language_code %>">
-  <%= htmlize(diary_entry.body) %>
+  <%= diary_entry.body.to_html %>
 </div>
 
 <% if diary_entry.latitude and diary_entry.longitude %>
index b506ddf..f477ab8 100644 (file)
@@ -11,7 +11,7 @@
   <tr class="<%= cl %>">
     <td><%= link_to comment.diary_entry.title, :action => :view, :display_name => comment.diary_entry.user.display_name, :id => comment.diary_entry.id %></td>
     <td><span title="<%= l comment.created_at, :format => :friendly %>"><%= t 'diary_entry.comments.ago', :ago => time_ago_in_words(comment.created_at) %></span></td>
-    <td><%= htmlize(comment.body) %></td>
+    <td><%= comment.body.to_html %></td>
   </tr>
   <% end -%>
 </table>
index 4c9670a..7ee36e9 100644 (file)
@@ -20,7 +20,7 @@ xml.rss("version" => "2.0",
         xml.title h(entry.title)
         xml.link url_for(:action => "view", :id => entry.id, :display_name => entry.user.display_name, :only_path => false)
         xml.guid url_for(:action => "view", :id => entry.id, :display_name => entry.user.display_name, :only_path => false)
-        xml.description htmlize(entry.body)
+        xml.description entry.body.to_html
         xml.author entry.user.display_name
         xml.pubDate entry.created_at.to_s(:rfc822)
         xml.comments url_for(:action => "view", :id => entry.id, :display_name => entry.user.display_name, :anchor => "comments", :only_path => false)
index 6bad015..d829c79 100644 (file)
@@ -20,7 +20,7 @@
   </tr>
   <tr>
     <th></th>
-    <td><%= htmlize(@message.body) %></td>
+    <td><%= @message.body.to_html %></td>
     <td></td>
   </tr>
 </table>
@@ -57,7 +57,7 @@
   </tr>
   <tr>
     <th></th>
-    <td><%= htmlize(@message.body) %></td>
+    <td><%= @message.body.to_html %></td>
     <td></td>
   </tr>
 </table>
index 193e10d..7d112a4 100644 (file)
@@ -3,7 +3,7 @@
 <%= t'notifier.diary_comment_notification.header', :from_user => @from_user, :subject => @title %>
 
 ==
-<%= raw @text %>
+<%= raw @text.to_text %>
 ==
 
 <%= t'notifier.diary_comment_notification.footer', :readurl => @readurl, :commenturl => @commenturl, :replyurl => @replyurl %>
index ef55b9b..0b2ff9e 100644 (file)
@@ -3,7 +3,7 @@
 <p><%= raw t'notifier.message_notification.header', :from_user => link_to(@from_user, :host => SERVER_URL, :controller => :user, :action => :view, :display_name => @from_user), :subject => @title %></p>
 
 ==
-<%= htmlize @text %>
+<%= @text.to_html %>
 ==
 
 <p>
index 25b3c04..10a9ff3 100644 (file)
@@ -3,7 +3,7 @@
 <%= raw t'notifier.message_notification.header', :from_user => @from_user, :subject => @title %>
 
 ==
-<%= raw @text %>
+<%= raw @text.to_text %>
 ==
 
 <%= raw t'notifier.message_notification.footer1', :readurl => @readurl %>
index 4f62a51..4f2f59c 100644 (file)
@@ -19,7 +19,7 @@
         %>
       <% end %>
     </p>
-    <%= htmlize(user.description) %>
+    <%= user.description.to_html %>
   </td>
   <td>
     <%= check_box_tag "user_#{user.id}", "", false, :name => "user[#{user.id}]" %>
index b323ff1..d9a65b3 100644 (file)
 
 <h3><%= t 'user.view.description' %></h3>
 
-<div id="description"><%= htmlize(@this_user.description) %></div>
+<div id="description"><%= @this_user.description.to_html %></div>
 
 <% if @user and @this_user.id == @user.id %>
   <div id="map" class="user_map">
index 4ba69a7..2311541 100644 (file)
@@ -18,7 +18,7 @@
 <p><b><%= t'user_block.show.status' %></b>: <%= block_status(@user_block) %></p>
 
 <p><b><%= t'user_block.show.reason' %></b></p>
-<%= htmlize(@user_block.reason) %>
+<%= @user_block.reason.to_html %>
 
 <% if @user_block.ends_at > Time.now.getutc %>
 <% if @user and @user.id == @user_block.creator_id %>
diff --git a/db/migrate/20120214210114_add_text_format.rb b/db/migrate/20120214210114_add_text_format.rb
new file mode 100644 (file)
index 0000000..f448223
--- /dev/null
@@ -0,0 +1,21 @@
+require 'migrate'
+
+class AddTextFormat < ActiveRecord::Migration
+  def up
+    create_enumeration :format_enum, ["html", "markdown"]
+    add_column :users, :description_format, :format_enum, :null => false, :default => "html"
+    add_column :user_blocks, :reason_format, :format_enum, :null => false, :default => "html"
+    add_column :diary_entries, :body_format, :format_enum, :null => false, :default => "html"
+    add_column :diary_comments, :body_format, :format_enum, :null => false, :default => "html"
+    add_column :messages, :body_format, :format_enum, :null => false, :default => "html"
+  end
+
+  def down
+    remove_column :messages, :body_format
+    remove_column :diary_comments, :body_format
+    remove_column :diary_entries, :body_format
+    remove_column :user_blocks, :reason_format
+    remove_column :users, :description_format
+    drop_enumeration :format_enum
+  end
+end
diff --git a/lib/rich_text.rb b/lib/rich_text.rb
new file mode 100644 (file)
index 0000000..ec5e9e4
--- /dev/null
@@ -0,0 +1,57 @@
+module RichText
+  def self.new(format, text)
+    case format
+    when "html"; HTML.new(text || "")
+    when "markdown"; Markdown.new(text || "")
+    else; nil
+    end
+  end
+
+  class HTML < String
+    include ActionView::Helpers::TextHelper
+    include ActionView::Helpers::TagHelper
+
+    def to_html
+      linkify(sanitize(simple_format(self)))
+    end
+
+    def to_text
+      self
+    end
+
+  private
+
+    def sanitize(text)
+      Sanitize.clean(text, Sanitize::Config::OSM).html_safe
+    end
+
+    def linkify(text)
+      if text.html_safe?
+        Rinku.auto_link(text, :urls, tag_options(:rel => "nofollow")).html_safe
+      else
+        Rinku.auto_link(text, :urls, tag_options(:rel => "nofollow"))
+      end
+    end
+  end
+
+  class Markdown < String
+    def to_html
+      html_parser.render(self).html_safe
+    end
+
+    def to_text
+      self
+    end
+
+  private
+
+    def html_parser
+      @@html_renderer ||= Redcarpet::Render::XHTML.new({
+        :filter_html => true, :safe_links_only => true
+      })
+      @@html_parser ||= Redcarpet::Markdown.new(@@html_renderer, {
+        :no_intra_emphasis => true, :autolink => true, :space_after_headers => true
+      })
+    end
+  end
+end