Introduce message renderer

This new class replaces all existing server side message
rendering helpers and is the new global entry point for such
needs. All models with relevant fields now expose an instance
of MessageRenderer for those. MessageRenderer acts as
gateway between the existing processing solutions for markdown,
mentions and tags and provides a very flexible interface for
all output needs. This makes the API to obtain a message
in a certain format clear. As a result of centralizing the
processing a lot of duplication is eliminated. Centralizing
the message processing also makes it clear where to change
its behaviour, add new representations and what options
are already available.
This commit is contained in:
Jonne Haß 2014-03-08 23:49:14 +01:00
parent 8f67e1eb17
commit 8280556a47
40 changed files with 545 additions and 389 deletions

View file

@ -65,7 +65,7 @@ gem 'messagebus_ruby_api', '1.0.3'
gem 'nokogiri', '1.6.1'
gem 'rails_autolink', '1.1.5'
gem 'redcarpet', '3.0.0'
gem 'redcarpet', '3.1.1'
gem 'roxml', '3.1.6'
gem 'ruby-oembed', '0.8.9'
gem 'opengraph_parser', '0.2.3'

View file

@ -345,7 +345,7 @@ GEM
rb-inotify (0.9.3)
rdoc (3.12.2)
json (~> 1.4)
redcarpet (3.0.0)
redcarpet (3.1.1)
redis (3.0.6)
redis-namespace (1.4.1)
redis (~> 3.0.4)
@ -512,7 +512,7 @@ DEPENDENCIES
rails_autolink (= 1.1.5)
rb-fsevent (= 0.9.4)
rb-inotify (= 0.9.3)
redcarpet (= 3.0.0)
redcarpet (= 3.1.1)
remotipart (= 1.2.1)
rmagick (= 2.13.2)
roxml (= 3.1.6)

View file

@ -98,7 +98,11 @@ class UsersController < ApplicationController
if @user = User.find_by_username(params[:username])
respond_to do |format|
format.atom do
@posts = Post.where(:author_id => @user.person_id, :public => true).order('created_at DESC').limit(25)
@posts = Post.where(author_id: @user.person_id, public: true)
.order('created_at DESC')
.limit(25)
.map {|post| post.is_a?(Reshare) ? post.absolute_root : post }
.compact
end
format.any { redirect_to person_path(@user.person) }

View file

@ -1,62 +0,0 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module MarkdownifyHelper
def markdown_options
{
:autolink => true,
:fenced_code_blocks => true,
:space_after_headers => true,
:strikethrough => true,
:tables => true,
:no_intra_emphasis => true,
}
end
def markdownify(target, render_options={})
render_options[:filter_html] = true
render_options[:hard_wrap] ||= true
render_options[:safe_links_only] = true
# This ugly little hack basically means
# "Give me the rawest contents of target available"
if target.respond_to?(:raw_message)
message = target.raw_message
elsif target.respond_to?(:text)
message = target.text
else
message = target.to_s
end
return '' if message.blank?
renderer = Diaspora::Markdownify::HTML.new(render_options)
markdown = Redcarpet::Markdown.new(renderer, markdown_options)
message = markdown.render(message).html_safe
if target.respond_to?(:mentioned_people)
message = Diaspora::Mentionable.format(message, target.mentioned_people)
end
message = Diaspora::Taggable.format_tags(message, :no_escape => true)
return message.html_safe
end
def strip_markdown(text)
renderer = Redcarpet::Markdown.new(Redcarpet::Render::StripDown, markdown_options)
renderer.render(text).strip
end
def process_newlines(message)
# in very clear cases, let newlines become <br /> tags
# Grabbed from Github flavored Markdown
message.gsub(/^[\w\<][^\n]*\n+/) do |x|
x =~ /\n{2}/ ? x : (x.strip!; x << " \n")
end
end
end

View file

@ -1,28 +1,20 @@
module NotifierHelper
# @param post [Post] The post object.
# @param opts [Hash] Optional hash. Accepts :length and :process_newlines parameters.
# @param opts [Hash] Optional hash. Accepts :length parameters.
# @return [String] The truncated and formatted post.
def post_message(post, opts={})
opts[:length] ||= 200
if post.respond_to? :formatted_message
message = strip_markdown(post.formatted_message(:plain_text => true))
message = truncate(message, :length => opts[:length])
message = process_newlines(message) if opts[:process_newlines]
message
if post.respond_to? :message
post.message.plain_text_without_markdown truncate: opts.fetch(:length, 200)
else
I18n.translate 'notifier.a_post_you_shared'
end
end
# @param comment [Comment] The comment to process.
# @param opts [Hash] Optional hash. Accepts :length and :process_newlines parameters.
# @param opts [Hash] Optional hash. Accepts :length parameters.
# @return [String] The truncated and formatted comment.
def comment_message(comment, opts={})
opts[:length] ||= 600
text = strip_markdown(comment.text)
text = truncate(text, :length => opts[:length])
text = process_newlines(text) if opts[:process_newlines]
text
comment.message.plain_text_without_markdown truncate: opts.fetch(:length, 600)
end
end

View file

@ -9,19 +9,8 @@ module PostsHelper
elsif post.is_a?(Reshare)
I18n.t "posts.show.reshare_by", :author => post.author_name
else
if post.text.present?
if opts.has_key?(:length)
truncate(post.text(:plain_text => true), :length => opts.fetch(:length))
elsif /\A(?: # Regexp to match a Markdown header present on first line :
(?<setext_content>.{1,200}\n(?:={1,200}|-{1,200}))(?:\r?\n|$) # Setext-style header
| # or
(?<atx_content>\#{1,6}\s.{1,200})(?:\r?\n|$) # Atx-style header
)/x =~ post.text(:plain_text => true)
return setext_content unless setext_content.nil?
return atx_content unless atx_content.nil?
else
truncate(post.text(:plain_text => true), :length => 20 )
end
if post.message.present?
post.message.title opts
elsif post.respond_to?(:photos) && post.photos.present?
I18n.t "posts.show.photos_by", :count => post.photos.size, :author => post.author_name
end

View file

@ -1,8 +1,5 @@
module NotificationMailers
class AlsoCommented < NotificationMailers::Base
include ActionView::Helpers::TextHelper
include MarkdownifyHelper
attr_accessor :comment
delegate :post, to: :comment, prefix: true
@ -11,8 +8,7 @@ module NotificationMailers
if mail?
@headers[:from] = "\"#{@comment.author_name} (diaspora*)\" <#{AppConfig.mail.sender_address}>"
@headers[:subject] = truncate(strip_markdown(@comment.comment_email_subject.squish), :length => TRUNCATION_LEN)
@headers[:subject] = "Re: #{@headers[:subject]}"
@headers[:subject] = "Re: #{@comment.comment_email_subject}"
end
end

View file

@ -1,6 +1,4 @@
module NotificationMailers
TRUNCATION_LEN = 70
class Base
attr_accessor :recipient, :sender

View file

@ -1,16 +1,12 @@
module NotificationMailers
class CommentOnPost < NotificationMailers::Base
include ActionView::Helpers::TextHelper
include MarkdownifyHelper
attr_accessor :comment
def set_headers(comment_id)
@comment = Comment.find(comment_id)
@headers[:from] = "\"#{@comment.author_name} (diaspora*)\" <#{AppConfig.mail.sender_address}>"
@headers[:subject] = truncate(strip_markdown(@comment.comment_email_subject.squish), :length => TRUNCATION_LEN)
@headers[:subject] = "Re: #{@headers[:subject]}"
@headers[:subject] = "Re: #{@comment.comment_email_subject}"
end
end
end

View file

@ -1,9 +1,8 @@
class Notifier < ActionMailer::Base
helper :application
helper :markdownify
helper :notifier
helper :people
def self.admin(string, recipients, opts = {})
mails = []
recipients.each do |rec|
@ -16,7 +15,7 @@ class Notifier < ActionMailer::Base
def single_admin(string, recipient, opts={})
@receiver = recipient
@string = string.html_safe
if attach = opts.delete(:attachments)
attach.each{ |f|
attachments[f[:name]] = f[:file]

View file

@ -5,7 +5,7 @@
class Comment < ActiveRecord::Base
include Diaspora::Federated::Base
include Diaspora::Guid
include Diaspora::Relayable
@ -22,7 +22,7 @@ class Comment < ActiveRecord::Base
belongs_to :commentable, :touch => true, :polymorphic => true
alias_attribute :post, :commentable
belongs_to :author, :class_name => 'Person'
delegate :name, to: :author, prefix: true
delegate :comment_email_subject, to: :parent
delegate :author_name, to: :parent, prefix: true
@ -79,6 +79,10 @@ class Comment < ActiveRecord::Base
self.post = parent
end
def message
@message ||= Diaspora::MessageRenderer.new text
end
def text= text
self[:text] = text.to_s.strip #to_s if for nil, for whatever reason
end

View file

@ -18,7 +18,7 @@ class Message < ActiveRecord::Base
validate :participant_of_parent_conversation
after_create do # don't use 'after_commit' here since there is a call to 'save!'
# inside, which would cause an infinite recursion
# inside, which would cause an infinite recursion
#sign comment as commenter
self.author_signature = self.sign_with_key(self.author.owner.encryption_key) if self.author.owner
@ -71,8 +71,8 @@ class Message < ActiveRecord::Base
Notifications::PrivateMessage unless user.person == person
end
def formatted_message(opts={})
opts[:plain_text] ? self.text: ERB::Util.h(self.text)
def message
@message ||= Diaspora::MessageRenderer.new text
end
private

View file

@ -55,7 +55,7 @@ class Profile < ActiveRecord::Base
def receive(user, person)
Rails.logger.info("event=receive payload_type=profile sender=#{person} to=#{user}")
profiles_attr = self.attributes.merge('tag_string' => self.tag_string).slice('diaspora_handle', 'first_name', 'last_name', 'image_url', 'image_url_small', 'image_url_medium', 'birthday', 'gender', 'bio', 'location', 'searchable', 'nsfw', 'tag_string')
person.profile.update_attributes(profiles_attr)
person.profile.update_attributes(profiles_attr)
person.profile
end
@ -78,13 +78,13 @@ class Profile < ActiveRecord::Base
def from_omniauth_hash(omniauth_user_hash)
mappings = {"description" => "bio",
'image' => 'image_url',
'name' => 'first_name',
'image' => 'image_url',
'name' => 'first_name',
'location' => 'location',
}
update_hash = Hash[ omniauth_user_hash.map {|k, v| [mappings[k], v] } ]
self.attributes.merge(update_hash){|key, old, new| old.blank? ? new : old}
end
@ -132,6 +132,13 @@ class Profile < ActiveRecord::Base
birthday.to_s(:long).gsub(', 1000', '') if birthday.present?
end
def bio_message
@bio_message ||= Diaspora::MessageRenderer.new(bio)
end
def location_message
@location_message ||= Diaspora::MessageRenderer.new(location)
end
def tag_string
if @tag_string

View file

@ -41,6 +41,10 @@ class Reshare < Post
self.root ? root.raw_message : super
end
def message
absolute_root.message if root.present?
end
def mentioned_people
self.root ? root.mentioned_people : super
end

View file

@ -3,11 +3,8 @@
# the COPYRIGHT file.
class Service < ActiveRecord::Base
include ActionView::Helpers::TextHelper
include MarkdownifyHelper
attr_accessor :provider, :info, :access_level
belongs_to :user
validates_uniqueness_of :uid, :scope => :type
@ -26,12 +23,12 @@ class Service < ActiveRecord::Base
end
def first_from_omniauth( auth_hash )
@@auth = auth_hash
@@auth = auth_hash
where( type: service_type, uid: options[:uid] ).first
end
def initialize_from_omniauth( auth_hash )
@@auth = auth_hash
@@auth = auth_hash
service_type.constantize.new( options )
end
@ -44,7 +41,7 @@ class Service < ActiveRecord::Base
end
def options
{
{
nickname: auth['info']['nickname'],
access_token: auth['credentials']['token'],
access_secret: auth['credentials']['secret'],

View file

@ -1,9 +1,8 @@
class Services::Facebook < Service
include Rails.application.routes.url_helpers
include MarkdownifyHelper
OVERRIDE_FIELDS_ON_FB_UPDATE = [:contact_id, :person_id, :request_id, :invitation_id, :photo_url, :name, :username]
MAX_CHARACTERS = 63206
MAX_CHARACTERS = 63206
def provider
"facebook"
@ -22,11 +21,16 @@ class Services::Facebook < Service
end
def create_post_params(post)
message = strip_markdown(post.text(:plain_text => true))
message = post.message.plain_text_without_markdown
if post.photos.any?
message += " " + Rails.application.routes.url_helpers.short_post_url(post, :protocol => AppConfig.pod_uri.scheme, :host => AppConfig.pod_uri.authority)
message += " " + short_post_url(post, protocol: AppConfig.pod_uri.scheme,
host: AppConfig.pod_uri.authority)
end
{:message => message, :access_token => self.access_token, :link => URI.extract(message, ['https', 'http']).first}
{message: message,
access_token: access_token,
link: URI.extract(message, ['https', 'http']).first
}
end
def profile_photo_url

View file

@ -1,7 +1,4 @@
class Services::Tumblr < Service
include ActionView::Helpers::TextHelper
include ActionView::Helpers::TagHelper
MAX_CHARACTERS = 1000
def provider
@ -38,10 +35,10 @@ class Services::Tumblr < Service
def tumblr_template(post, url)
html = ''
post.photos.each do |photo|
html += "![photo](#{photo.url(:scaled_full)})\n\n"
html << "![photo](#{photo.url(:scaled_full)})\n\n"
end
html += post.text
html += "\n\n[original post](#{url})"
html << post.message.html(mentioned_people: [])
html << "\n\n[original post](#{url})"
end
def delete_post(post)

View file

@ -1,7 +1,5 @@
class Services::Twitter < Service
include ActionView::Helpers::TextHelper
include Rails.application.routes.url_helpers
include MarkdownifyHelper
MAX_CHARACTERS = 140
SHORTENED_URL_LENGTH = 21
@ -40,7 +38,7 @@ class Services::Twitter < Service
def attempt_post post, retry_count=0
message = build_twitter_post post, retry_count
tweet = client.update message
client.update message
rescue Twitter::Error::Forbidden => e
if ! e.message.include? 'is over 140' || retry_count == 20
raise e
@ -52,7 +50,7 @@ class Services::Twitter < Service
def build_twitter_post post, retry_count=0
max_characters = MAX_CHARACTERS - retry_count
post_text = strip_markdown post.text(plain_text: true)
post_text = post.message.plain_text_without_markdown
truncate_and_add_post_link post, post_text, max_characters
end
@ -65,7 +63,7 @@ class Services::Twitter < Service
host: AppConfig.pod_uri.authority
)
truncated_text = truncate post_text, length: max_characters - SHORTENED_URL_LENGTH + 1
truncated_text = post_text.truncate max_characters - SHORTENED_URL_LENGTH + 1
truncated_text = restore_truncated_url truncated_text, post_text, max_characters
"#{truncated_text} #{post_url}"
@ -95,11 +93,9 @@ class Services::Twitter < Service
return truncated_text if truncated_text !~ /#{LINK_PATTERN}\Z/
url = post_text.match(LINK_PATTERN, truncated_text.rindex('http'))[0]
truncated_text = truncate(
post_text,
length: max_characters - SHORTENED_URL_LENGTH + 2,
separator: ' ',
omission: ''
truncated_text = post_text.truncate(
max_characters - SHORTENED_URL_LENGTH + 2,
separator: ' ', omission: ''
)
"#{truncated_text} #{url} ..."

View file

@ -1,32 +1,30 @@
class Services::Wordpress < Service
include ActionView::Helpers::TextHelper
include MarkdownifyHelper
MAX_CHARACTERS = 1000
attr_accessor :username, :password, :host, :path
# uid = blog_id
def provider
"wordpress"
end
def post(post, url='')
res = Faraday.new(:url => "https://public-api.wordpress.com").post do |req|
def post post, url=''
res = Faraday.new(url: "https://public-api.wordpress.com").post do |req|
req.url "/rest/v1/sites/#{self.uid}/posts/new"
req.body = post_body(post).to_json
req.headers['Authorization'] = "Bearer #{self.access_token}"
req.headers['Content-Type'] = 'application/json'
end
JSON.parse res.env[:body]
end
def post_body(post, url='')
post_text = markdownify(post.text)
post_title = truncate(strip_markdown(post.text(:plain_text => true)), :length => 40, :separator => ' ')
{:title => post_title, :content => post_text.html_safe}
def post_body post
{
title: post.message.title(length: 40),
content: post.message.markdownified(disable_hovercards: true)
}
end
end

View file

@ -5,7 +5,6 @@
class StatusMessage < Post
include Diaspora::Taggable
include ActionView::Helpers::TextHelper
include PeopleHelper
acts_as_taggable_on :tags
@ -56,10 +55,6 @@ class StatusMessage < Post
tag_stream(tag_ids)
end
def text(opts = {})
self.formatted_message(opts)
end
def raw_message
read_attribute(:text)
end
@ -77,12 +72,8 @@ class StatusMessage < Post
self.raw_message.match(/#nsfw/i) || super
end
def formatted_message(opts={})
return self.raw_message unless self.raw_message
escaped_message = opts[:plain_text] ? self.raw_message : ERB::Util.h(self.raw_message)
mentioned_message = Diaspora::Mentionable.format(escaped_message, self.mentioned_people, opts)
Diaspora::Taggable.format_tags(mentioned_message, opts.merge(:no_escape => true))
def message
@message ||= Diaspora::MessageRenderer.new raw_message, mentioned_people: mentioned_people
end
def mentioned_people
@ -134,7 +125,7 @@ class StatusMessage < Post
end
def comment_email_subject
formatted_message(:plain_text => true)
message.title length: 70
end
def first_photo_url(*args)
@ -142,7 +133,7 @@ class StatusMessage < Post
end
def text_and_photos_blank?
self.text.blank? && self.photos.blank?
self.raw_message.blank? && self.photos.blank?
end
def queue_gather_oembed_data

View file

@ -1,6 +1,5 @@
class OEmbedPresenter
include PostsHelper
include ActionView::Helpers::TextHelper
def initialize(post, opts = {})
@post = post
@ -13,14 +12,14 @@ class OEmbedPresenter
def as_json(opts={})
{
:provider_name => "Diaspora",
:provider_name => "Diaspora",
:provider_url => AppConfig.pod_uri.to_s,
:type => 'rich',
:version => '1.0',
:title => post_title,
:author_name => post_author,
:author_url => post_author_url,
:width => @opts.fetch(:maxwidth, 516),
:width => @opts.fetch(:maxwidth, 516),
:height => @opts.fetch(:maxheight, 320),
:html => iframe_html
}

View file

@ -1,6 +1,5 @@
class PostPresenter
include PostsHelper
include ActionView::Helpers::TextHelper
attr_accessor :post, :current_user
@ -47,7 +46,7 @@ class PostPresenter
end
def title
@post.text.present? ? post_page_title(@post) : I18n.translate('posts.presenter.title', :name => @post.author_name)
@post.message.present? ? @post.message.title : I18n.t('posts.presenter.title', name: @post.author_name)
end
def root

View file

@ -16,4 +16,4 @@
= timeago(comment.created_at ? comment.created_at : Time.now)
%div{:class => direction_for(comment.text)}
= markdownify(comment)
= comment.message.markdownified

View file

@ -11,4 +11,4 @@
= timeago(message.created_at)
%div{ :class => direction_for(message.text) }
= markdownify(message, :oembed => true)
= message.message.markdownified

View file

@ -15,35 +15,35 @@
.profile_button
= link_to content_tag(:div, nil, :class => 'icons-mention', :title => t('people.show.mention'), :id => 'mention_button'), new_status_message_path(:person_id => @person.id), :rel => 'facebox'
.white_bar
- if @contact.mutual?
.profile_button
= link_to content_tag(:div, nil, :class => 'icons-message', :title => t('people.show.message'), :id => 'message_button'), new_conversation_path(:contact_id => @contact.id, :name => @contact.person.name), :rel => 'facebox'
.white_bar
.profile_button
= link_to content_tag(:div, nil, :class => 'icons-ignoreuser block_user', :title => t('ignore'), :id => 'block_user_button', :data => { :person_id => @person.id }), '#', :rel => "nofollow" if @block.blank?
%br
%br
-if contact.sharing? || person == current_user.person
%ul#profile_information
- unless person.bio.blank?
%li
%h4
=t('.bio')
%div{ :class => direction_for(person.bio) }
= markdownify(person.profile.bio, :oembed => true, :newlines => true)
= person.profile.bio_message.markdownified
- unless person.profile.location.blank?
%li
%h4
=t('.location')
%div{ :class => direction_for(person.location) }
= markdownify(person.location, :oembed => true, :newlines => true)
= person.profile.location_message.markdownified
- unless person.gender.blank?
%li
@ -66,7 +66,7 @@
= image_tag(photo.url(:thumb_small))
%br
= link_to t('layouts.header.view_all'), person_photos_path(person)
- if person == current_user.person
%li.image_list
%h4

View file

@ -15,7 +15,7 @@
= image_tag post.image_url
%div{:class => direction_for(post.text)}
!= markdownify(post)
!= post.message.markdownified
- if post.o_embed_cache
!= o_embed_html post.o_embed_cache
-if post.open_graph_cache

View file

@ -27,12 +27,11 @@ atom_feed({'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
end
@posts.each do |post|
post = post.absolute_root unless post.absolute_root.nil? if post.is_a? Reshare
feed.entry post, :url => "#{@user.url}p/#{post.id}",
:id => "#{@user.url}p/#{post.id}" do |entry|
entry.title post_page_title(post)
entry.content markdownify(post), :type => 'html'
entry.title post.message.title
entry.content post.message.markdownified(disable_hovercards: true), :type => 'html'
entry.tag! 'activity:verb', 'http://activitystrea.ms/schema/1.0/post'
entry.tag! 'activity:object-type', 'http://activitystrea.ms/schema/1.0/note'
end

View file

@ -13,9 +13,6 @@ require 'typhoeus'
# Presenters
require 'post_presenter'
# Helpers
require 'markdownify_helper'
# Our libs
require 'collect_user_photos'
require 'diaspora'

View file

@ -7,6 +7,7 @@ module Diaspora
require 'diaspora/parser'
require 'diaspora/fetcher'
require 'diaspora/markdownify'
require 'diaspora/message_renderer'
require 'diaspora/mentionable'
require 'diaspora/exporter'
require 'diaspora/federated'

View file

@ -2,12 +2,10 @@ module Diaspora
module Markdownify
class HTML < Redcarpet::Render::HTML
include ActionView::Helpers::TextHelper
include ActionView::Helpers::TagHelper
def autolink(link, type)
auto_link(link, :link => :urls, :html => { :target => "_blank" })
def autolink link, type
auto_link(link, link: :urls, html: { target: "_blank" })
end
end
end
end

View file

@ -0,0 +1,233 @@
module Diaspora
# Takes a raw message text and converts it to
# various desired target formats, respecting
# all possible formatting options supported
# by Diaspora
class MessageRenderer
class Processor
class << self
private :new
def process message, options, &block
return '' if message.blank? # Optimize for empty message
processor = new message, options
processor.instance_exec(&block)
processor.message
end
end
attr_reader :message, :options
def initialize message, options
@message = message
@options = options
end
def squish
@message = message.squish if options[:squish]
end
def append_and_truncate
if options[:truncate]
@message = message.truncate options[:truncate]-options[:append].to_s.size
end
message << options[:append].to_s
message << options[:append_after_truncate].to_s
end
include ActionView::Helpers::TagHelper
def escape
if options[:escape]
# TODO: On Rails 4 port change this to ERB::Util.html_escape_once
# and remove the include
@message = escape_once message
# Special case Hex entities since escape_once
# doesn't catch them.
# TODO: Watch for https://github.com/rails/rails/pull/9102
# on whether this can be removed
@message = message.gsub(/&amp;(#[xX][\dA-Fa-f]{1,4});/, '&\1;')
end
end
def strip_markdown
renderer = Redcarpet::Markdown.new Redcarpet::Render::StripDown, options[:markdown_options]
@message = renderer.render(message).strip
end
def markdownify
renderer = Diaspora::Markdownify::HTML.new options[:markdown_render_options]
markdown = Redcarpet::Markdown.new renderer, options[:markdown_options]
@message = markdown.render message
end
# In very clear cases, let newlines become <br /> tags
# Grabbed from Github flavored Markdown
def process_newlines
message.gsub(/^[\w\<][^\n]*\n+/) do |x|
x =~ /\n{2}/ ? x : (x.strip!; x << " \n")
end
end
def render_mentions
unless options[:disable_hovercards] || options[:mentioned_people].empty?
@message = Diaspora::Mentionable.format message, options[:mentioned_people]
end
if options[:disable_hovercards] || options[:link_all_mentions]
@message = Diaspora::Mentionable.filter_for_aspects message, nil
else
make_mentions_plain_text
end
end
def make_mentions_plain_text
@message = Diaspora::Mentionable.format message, [], plain_text: true
end
def render_tags
@message = Diaspora::Taggable.format_tags message, no_escape: !options[:escape_tags]
end
end
DEFAULTS = {mentioned_people: [],
link_all_mentions: false,
disable_hovercards: false,
truncate: false,
append: nil,
append_after_truncate: nil,
squish: false,
escape: true,
escape_tags: false,
markdown_options: {
autolink: true,
fenced_code_blocks: true,
space_after_headers: true,
strikethrough: true,
tables: true,
no_intra_emphasis: true,
},
markdown_render_options: {
filter_html: true,
hard_wrap: true,
safe_links_only: true
}}.freeze
delegate :empty?, :blank?, :present?, to: :raw
# @param [String] raw_message Raw input text
# @param [Hash] opts Global options affecting output
# @option opts [Array<Person>] :mentioned_people ([]) List of people
# allowed to mention
# @option opts [Boolean] :link_all_mentions (false) Whether to link
# all mentions. This makes plain links to profiles for people not in
# :mentioned_people
# @option opts [Boolean] :disable_hovercards (true) Render all mentions
# as profile links. This implies :link_all_mentions and ignores
# :mentioned_people
# @option opts [#to_i, Boolean] :truncate (false) Truncate message to
# the specified length
# @option opts [String] :append (nil) Append text to the end of
# the (truncated) message, counts into truncation length
# @option opts [String] :append_after_truncate (nil) Append text to the end
# of the (truncated) message, doesn't count into the truncation length
# @option opts [Boolean] :squish (false) Squish the message, that is
# remove all surrounding and consecutive whitespace
# @option opts [Boolean] :escape (true) Escape HTML relevant characters
# in the message. Note that his option is ignored in the plaintext
# renderers.
# @option opts [Boolean] :escape_tags (false) Escape HTML relevant
# characters in tags when rendering them
# @option opts [Hash] :markdown_options Override default options passed
# to Redcarpet
# @option opts [Hash] :markdown_render_options Override default options
# passed to the Redcarpet renderer
def initialize raw_message, opts={}
@raw_message = raw_message
@options = DEFAULTS.deep_merge opts
end
# @param [Hash] opts Override global output options, see {#initialize}
def plain_text opts={}
process(opts) {
make_mentions_plain_text
squish
append_and_truncate
}
end
# @param [Hash] opts Override global output options, see {#initialize}
def plain_text_without_markdown opts={}
process(opts) {
make_mentions_plain_text
strip_markdown
squish
append_and_truncate
}
end
# @param [Hash] opts Override global output options, see {#initialize}
def html opts={}
process(opts) {
escape
render_mentions
render_tags
squish
append_and_truncate
}.html_safe
end
# @param [Hash] opts Override global output options, see {#initialize}
def markdownified opts={}
process(opts) {
process_newlines
markdownify
render_mentions
render_tags
squish
append_and_truncate
}.html_safe
end
# Get a short summary of the message
# @param [Hash] opts Additional options
# @option opts [Integer] :length (20 | first heading) Truncate the title to
# this length. If not given defaults to 20 and to not truncate
# if a heading is found.
def title opts={}
# Setext-style header
heading = if /\A(?<setext_content>.{1,200})\n(?:={1,200}|-{1,200})(?:\r?\n|$)/ =~ @raw_message.lstrip
setext_content
# Atx-style header
elsif /\A\#{1,6}\s+(?<atx_content>.{1,200}?)(?:\s+#+)?(?:\r?\n|$)/ =~ @raw_message.lstrip
atx_content
end
heading &&= heading.strip
if heading && opts[:length]
heading.truncate opts[:length]
elsif heading
heading
else
plain_text_without_markdown squish: true, truncate: opts.fetch(:length, 20)
end
end
def raw
@raw_message
end
def to_s
plain_text
end
private
def process opts, &block
Processor.process(@raw_message, @options.deep_merge(opts), &block)
end
end
end

View file

@ -1,105 +0,0 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require 'spec_helper'
describe MarkdownifyHelper do
describe "#markdownify" do
describe "not doing something dumb" do
it "strips out script tags" do
markdownify("<script>alert('XSS is evil')</script>").should ==
"<p>alert(&#39;XSS is evil&#39;)</p>\n"
end
it 'strips onClick handlers from links' do
omghax = '[XSS](http://joindiaspora.com/" onClick="$\(\'a\'\).remove\(\))'
markdownify(omghax).should_not match(/ onClick/i)
end
end
it 'does not barf if message is nil' do
markdownify(nil).should == ''
end
it 'autolinks standard url links' do
markdownified = markdownify("http://joindiaspora.com/")
doc = Nokogiri.parse(markdownified)
link = doc.css("a")
link.attr("href").value.should == "http://joindiaspora.com/"
end
context 'when formatting status messages' do
it "should leave tags intact" do
message = FactoryGirl.create(:status_message,
:author => alice.person,
:text => "I love #markdown")
formatted = markdownify(message)
formatted.should =~ %r{<a href="/tags/markdown" class="tag">#markdown</a>}
end
it 'should leave multi-underscore tags intact' do
message = FactoryGirl.create(
:status_message,
:author => alice.person,
:text => "Here is a #multi_word tag"
)
formatted = markdownify(message)
formatted.should =~ %r{Here is a <a href="/tags/multi_word" class="tag">#multi_word</a> tag}
message = FactoryGirl.create(
:status_message,
:author => alice.person,
:text => "Here is a #multi_word_tag yo"
)
formatted = markdownify(message)
formatted.should =~ %r{Here is a <a href="/tags/multi_word_tag" class="tag">#multi_word_tag</a> yo}
end
it "should leave mentions intact" do
message = FactoryGirl.create(:status_message,
:author => alice.person,
:text => "Hey @{Bob; #{bob.diaspora_handle}}!")
formatted = markdownify(message)
formatted.should =~ /hovercard/
end
it "should leave mentions intact for real diaspora handles" do
new_person = FactoryGirl.create(:person, :diaspora_handle => 'maxwell@joindiaspora.com')
message = FactoryGirl.create(:status_message,
:author => alice.person,
:text => "Hey @{maxwell@joindiaspora.com; #{new_person.diaspora_handle}}!")
formatted = markdownify(message)
formatted.should =~ /hovercard/
end
it 'should process text with both a hashtag and a link' do
message = FactoryGirl.create(:status_message,
:author => alice.person,
:text => "Test #tag?\nhttps://joindiaspora.com\n")
formatted = markdownify(message)
formatted.should == %{<p>Test <a href="/tags/tag" class="tag">#tag</a>?<br>\n<a href="https://joindiaspora.com" target="_blank">https://joindiaspora.com</a></p>\n}
end
it 'should process text with a header' do
message = "# I love markdown"
markdownify(message).should match "I love markdown"
end
end
end
describe "#strip_markdown" do
it 'does not remove markdown in links' do
message = "some text and here comes http://exampe.org/foo_bar_baz a link"
strip_markdown(message).should match message
end
it 'does not destroy hashtag that starts a line' do
message = "#hashtag message"
strip_markdown(message).should match message
end
end
end

View file

@ -5,8 +5,6 @@
require 'spec_helper'
describe NotifierHelper do
include MarkdownifyHelper
describe '#post_message' do
before do
# post for truncate test

View file

@ -12,30 +12,11 @@ describe PostsHelper do
end
context 'with posts with text' do
context 'when :length is passed in parameters' do
it 'returns string of size less or equal to :length' do
@sm = double(:text => "## My title\n Post content...")
string_size = 12
post_page_title(@sm, :length => string_size ).size.should <= string_size
end
end
context 'when :length is not passed in parameters' do
context 'with a Markdown header of less than 200 characters on first line'do
it 'returns atx style header' do
@sm = double(:text => "## My title\n Post content...")
post_page_title(@sm).should == "## My title"
end
it 'returns setext style header' do
@sm = double(:text => "My title \n======\n Post content...")
post_page_title(@sm).should == "My title \n======"
end
end
context 'without a Markdown header of less than 200 characters on first line 'do
it 'truncates posts to the 20 first characters' do
@sm = double(:text => "Very, very, very long post")
post_page_title(@sm).should == "Very, very, very ..."
end
end
it "delegates to message.title" do
message = double
message.should_receive(:title)
post = double(message: message)
post_page_title(post)
end
end
end

View file

@ -0,0 +1,175 @@
require 'spec_helper'
describe Diaspora::MessageRenderer do
def message text, opts={}
Diaspora::MessageRenderer.new(text, opts)
end
describe '#title' do
context 'when :length is passed in parameters' do
it 'returns string of size less or equal to :length' do
string_size = 12
title = message("## My title\n Post content...").title(length: string_size)
expect(title.size).to be <= string_size
end
end
context 'when :length is not passed in parameters' do
context 'with a Markdown header of less than 200 characters on first line' do
it 'returns atx style header' do
expect(message("## My title\n Post content...").title).to eq "My title"
expect(message("## My title ##\n Post content...").title).to eq "My title"
end
it 'returns setext style header' do
expect(message("My title \n======\n Post content...").title).to eq "My title"
end
end
context 'without a Markdown header of less than 200 characters on first line ' do
it 'truncates posts to the 20 first characters' do
expect(message("Very, very, very long post").title).to eq "Very, very, very ..."
end
end
end
end
describe '#html' do
it 'escapes the message' do
xss = "</a> <script> alert('hey'); </script>"
expect(message(xss).html).to_not include xss
end
it 'is html_safe' do
expect(message("hey guys").html).to be_html_safe
end
it 'should leave HTML entities intact' do
entities = '&amp; &szlig; &#x27; &#39; &quot;'
expect(message(entities).html).to eq entities
end
context 'with mentions' do
it 'makes hovercard links for mentioned people' do
expect(
message(
"@{Bob; #{bob.person.diaspora_handle}}",
mentioned_people: [bob.person]
).html
).to include 'hovercard'
end
it 'makes plaintext out of mentions of people not in the posts aspects' do
expect(
message("@{Bob; #{bob.person.diaspora_handle}}").html
).to_not include 'href'
end
context 'linking all mentions' do
it 'makes plain links for people not in the post aspects' do
message = message("@{Bob; #{bob.person.diaspora_handle}}", link_all_mentions: true).html
expect(message).to_not include 'hovercard'
expect(message).to include '/u/bob'
end
it "makes no hovercards if they're disabled" do
message = message(
"@{Bob; #{bob.person.diaspora_handle}}",
mentioned_people: [bob.person],
disable_hovercards: true
).html
expect(message).to_not include 'hovercard'
expect(message).to include '/u/bob'
end
end
end
end
describe "#markdownified" do
describe "not doing something dumb" do
it "strips out script tags" do
expect(
message("<script>alert('XSS is evil')</script>").markdownified
).to eq "<p>alert(&#39;XSS is evil&#39;)</p>\n"
end
it 'strips onClick handlers from links' do
expect(
message('[XSS](http://joindiaspora.com/" onClick="$\(\'a\'\).remove\(\))').markdownified
).to_not match(/ onClick/i)
end
end
it 'does not barf if message is nil' do
expect(message(nil).markdownified).to eq ''
end
it 'autolinks standard url links' do
expect(
message("http://joindiaspora.com/"
).markdownified).to include 'href="http://joindiaspora.com/"'
end
context 'when formatting status messages' do
it "should leave tags intact" do
expect(
message("I love #markdown").markdownified
).to match %r{<a href="/tags/markdown" class="tag">#markdown</a>}
end
it 'should leave multi-underscore tags intact' do
expect(
message("Here is a #multi_word tag").markdownified
).to match %r{Here is a <a href="/tags/multi_word" class="tag">#multi_word</a> tag}
expect(
message("Here is a #multi_word_tag yo").markdownified
).to match %r{Here is a <a href="/tags/multi_word_tag" class="tag">#multi_word_tag</a> yo}
end
it "should leave mentions intact" do
expect(
message("Hey @{Bob; #{bob.diaspora_handle}}!", mentioned_people: [bob.person]).markdownified
).to match(/hovercard/)
end
it "should leave mentions intact for real diaspora handles" do
new_person = FactoryGirl.create(:person, diaspora_handle: 'maxwell@joindiaspora.com')
expect(
message(
"Hey @{maxwell@joindiaspora.com; #{new_person.diaspora_handle}}!",
mentioned_people: [new_person]
).markdownified
).to match(/hovercard/)
end
it 'should process text with both a hashtag and a link' do
expect(
message("Test #tag?\nhttps://joindiaspora.com\n").markdownified
).to eq %{<p>Test <a href="/tags/tag" class="tag">#tag</a>?<br>\n<a href="https://joindiaspora.com" target="_blank">https://joindiaspora.com</a></p>\n}
end
it 'should process text with a header' do
expect(message("# I love markdown").markdownified).to match "I love markdown"
end
it 'should leave HTML entities intact' do
entities = '&amp; &szlig; &#x27; &#39; &quot;'
expect(message(entities).markdownified).to eq "<p>#{entities}</p>\n"
end
end
end
describe "#plain_text_without_markdown" do
it 'does not remove markdown in links' do
text = "some text and here comes http://exampe.org/foo_bar_baz a link"
expect(message(text).plain_text_without_markdown).to eq text
end
it 'does not destroy hashtag that starts a line' do
text = "#hashtag message"
expect(message(text).plain_text_without_markdown).to eq text
end
end
end

View file

@ -1,9 +1,6 @@
require 'spec_helper'
describe Notifier do
include ActionView::Helpers::TextHelper
include MarkdownifyHelper
let(:person) { FactoryGirl.create(:person) }
before do
@ -120,7 +117,7 @@ describe Notifier do
end
it 'BODY: contains the truncated original post' do
@mail.body.encoded.should include(@sm.formatted_message)
@mail.body.encoded.should include(@sm.message.plain_text)
end
it 'BODY: contains the name of person liking' do
@ -150,7 +147,7 @@ describe Notifier do
end
it 'BODY: contains the truncated original post' do
@mail.body.encoded.should include(@sm.formatted_message)
@mail.body.encoded.should include(@sm.message.plain_text)
end
it 'BODY: contains the name of person liking' do
@ -224,7 +221,7 @@ describe Notifier do
end
it 'SUBJECT: has a snippet of the post contents, without markdown and without newlines' do
comment_mail.subject.should == "Re: Headline It's really sunny outside today, and this is a super long ..."
comment_mail.subject.should == "Re: Headline"
end
context 'BODY' do
@ -265,7 +262,7 @@ describe Notifier do
end
it 'SUBJECT: has a snippet of the post contents, without markdown and without newlines' do
comment_mail.subject.should == "Re: Headline It's really sunny outside today, and this is a super long ..."
comment_mail.subject.should == "Re: Headline"
end
context 'BODY' do

View file

@ -25,17 +25,17 @@ describe Services::Facebook do
end
it 'removes text formatting markdown from post text' do
message = "Text with some **bolded** and _italic_ parts."
post = double(:text => message, :photos => [])
message = double
message.should_receive(:plain_text_without_markdown).and_return("")
post = double(message: message, photos: [])
post_params = @service.create_post_params(post)
post_params[:message].should match "Text with some bolded and italic parts."
end
it 'does not add post link when no photos' do
message = "Text with some **bolded** and _italic_ parts."
post = double(:text => message, :photos => [])
message = "Some text."
post = double(message: double(plain_text_without_markdown: message), photos: [])
post_params = @service.create_post_params(post)
post_params[:message].should match "Text with some bolded and italic parts."
post_params[:message].should_not include "http"
end
it 'sets facebook id on post' do

View file

@ -38,9 +38,10 @@ describe Services::Twitter do
end
it 'removes text formatting markdown from post text' do
message = "Text with some **bolded** and _italic_ parts."
post = double(:text => message, :photos => [])
@service.send(:build_twitter_post, post).should match "Text with some bolded and italic parts."
message = double
message.should_receive(:plain_text_without_markdown).and_return("")
post = double(message: message, photos: [])
@service.send(:build_twitter_post, post)
end
end
@ -53,19 +54,19 @@ describe Services::Twitter do
it "should not truncate a short message" do
short_message = SecureRandom.hex(20)
short_post = double(:text => short_message, :photos => [])
short_post = double(message: double(plain_text_without_markdown: short_message), photos: [])
@service.send(:build_twitter_post, short_post).should match short_message
end
it "should truncate a long message" do
long_message = SecureRandom.hex(220)
long_post = double(:text => long_message, :id => 1, :photos => [])
long_post = double(message: double(plain_text_without_markdown: long_message), id: 1, photos: [])
@service.send(:build_twitter_post, long_post).length.should be < long_message.length
end
it "should not truncate a long message with an http url" do
long_message = " http://joindiaspora.com/a-very-long-url-name-that-will-be-shortened.html " + @long_message_end
long_post = double(:text => long_message, :id => 1, :photos => [])
long_post = double(message: double(plain_text_without_markdown: long_message), id: 1, photos: [])
@post.text = long_message
answer = @service.send(:build_twitter_post, @post)
@ -74,7 +75,7 @@ describe Services::Twitter do
it "should not cut links when truncating a post" do
long_message = SecureRandom.hex(40) + " http://joindiaspora.com/a-very-long-url-name-that-will-be-shortened.html " + SecureRandom.hex(55)
long_post = double(:text => long_message, :id => 1, :photos => [])
long_post = double(message: double(plain_text_without_markdown: long_message), id: 1, photos: [])
answer = @service.send(:build_twitter_post, long_post)
answer.should match /\.\.\./
@ -83,7 +84,7 @@ describe Services::Twitter do
it "should append the otherwise-cut link when truncating a post" do
long_message = "http://joindiaspora.com/a-very-long-decoy-url.html " + SecureRandom.hex(20) + " http://joindiaspora.com/a-very-long-url-name-that-will-be-shortened.html " + SecureRandom.hex(55) + " http://joindiaspora.com/a-very-long-decoy-url-part-2.html"
long_post = double(:text => long_message, :id => 1, :photos => [])
long_post = double(message: double(plain_text_without_markdown: long_message), id: 1, photos: [])
answer = @service.send(:build_twitter_post, long_post)
answer.should match /\.\.\./
@ -99,7 +100,7 @@ describe Services::Twitter do
it "should truncate a long message with an ftp url" do
long_message = @long_message_start + " ftp://joindiaspora.com/a-very-long-url-name-that-will-be-shortened.html " + @long_message_end
long_post = double(:text => long_message, :id => 1, :photos => [])
long_post = double(message: double(plain_text_without_markdown: long_message), id: 1, photos: [])
answer = @service.send(:build_twitter_post, long_post)
answer.should match /\.\.\./
@ -107,7 +108,7 @@ describe Services::Twitter do
it "should not truncate a message of maximum length" do
exact_size_message = SecureRandom.hex(70)
exact_size_post = double(:text => exact_size_message, :id => 1, :photos => [])
exact_size_post = double(message: double(plain_text_without_markdown: exact_size_message), id: 1, photos: [])
answer = @service.send(:build_twitter_post, exact_size_post)
answer.should match exact_size_message

View file

@ -150,18 +150,6 @@ STR
@sm = FactoryGirl.create(:status_message, :text => @test_string )
end
describe '#formatted_message' do
it 'escapes the message' do
xss = "</a> <script> alert('hey'); </script>"
@sm.text << xss
@sm.formatted_message.should_not include xss
end
it 'is html_safe' do
@sm.formatted_message.html_safe?.should be_true
end
end
describe '#create_mentions' do
it 'creates a mention for everyone mentioned in the message' do
Diaspora::Mentionable.should_receive(:people_from_string).and_return(@people)

View file

@ -65,32 +65,17 @@ describe PostPresenter do
describe '#title' do
context 'with posts with text' do
context 'with a Markdown header of less than 200 characters on first line'do
it 'returns atx style header' do
@sm = double(:text => "## My title\n Post content...")
@presenter.post = @sm
@presenter.title.should == "## My title"
end
it 'returns setext style header' do
@sm = double(:text => "My title \n======\n Post content...")
@presenter.post = @sm
@presenter.title.should == "My title \n======"
end
end
context 'without a Markdown header of less than 200 characters on first line 'do
it 'truncates post to the 20 first characters' do
@sm = double(:text => "Very, very, very long post")
@presenter.post = @sm
@presenter.title.should == "Very, very, very ..."
end
it "delegates to message.title" do
message = double(present?: true)
message.should_receive(:title)
@presenter.post = double(message: message)
@presenter.title
end
end
context 'with posts without text' do
it ' displays a messaage with the post class' do
@sm = double(:text => "", :author => bob.person, :author_name => bob.person.name)
@sm = double(message: double(present?: false), author: bob.person, author_name: bob.person.name)
@presenter.post = @sm
@presenter.title.should == "A post from #{@sm.author.name}"
end