[#19] add gitlabhq markdown

This commit is contained in:
Alexander Machehin 2013-03-29 00:22:24 +06:00
parent 76e19f717c
commit e78e73cae8
11 changed files with 329 additions and 15 deletions

View File

@ -23,12 +23,11 @@ gem 'state_machine'
gem 'grack', :git => 'git://github.com/rdblue/grack.git', :require => 'git_http'
gem "grit", :git => 'git://github.com/warpc/grit.git' #, :path => '~/Sites/code/grit'
gem 'charlock_holmes', '~> 0.6.9' #, :git => 'git://github.com/brianmario/charlock_holmes.git', :branch => 'bundle-icu'
gem 'github-linguist', '~> 2.2.1', :require => 'linguist'
gem 'github-linguist', '~> 2.3.4', :require => 'linguist'
gem 'diff-display', '~> 0.0.1'
# Wiki
gem "gollum", '~> 2.1.3'
gem "redcarpet", "~> 2.1.1"
gem 'creole'
gem 'rdiscount'
# gem 'org-ruby'
@ -54,6 +53,9 @@ gem 'rest-client', '~> 1.6.6'
gem 'attr_encrypted', '1.2.1'
gem "gitlab-pygments.rb", '~> 0.3.2', require: 'pygments.rb'
gem "redcarpet", "~> 2.2.2"
group :assets do
gem 'sass-rails', '~> 3.2.5'
gem 'coffee-rails', '~> 3.2.2'
@ -62,6 +64,7 @@ group :assets do
gem 'therubyracer', '~> 0.10.2', :platforms => [:mri, :rbx]
gem 'therubyrhino', '~> 1.73.1', :platforms => :jruby
gem 'turbo-sprockets-rails3'
gem "gemoji", "~> 1.2.1", require: 'emoji/railtie'
end
group :production do

View File

@ -121,13 +121,17 @@ GEM
railties (>= 3.0.0)
ffi (1.0.11)
fssm (0.2.10)
github-linguist (2.2.1)
gemoji (1.2.1)
github-linguist (2.3.4)
charlock_holmes (~> 0.6.6)
escape_utils (~> 0.2.3)
mime-types (~> 1.18)
mime-types (~> 1.19)
pygments.rb (>= 0.2.13)
github-markdown (0.5.3)
github-markup (0.7.5)
gitlab-pygments.rb (0.3.2)
posix-spawn (~> 0.3.6)
yajl-ruby (~> 1.1.0)
gollum (2.1.10)
github-markdown
github-markup (>= 0.7.0, < 1.0.0)
@ -257,7 +261,7 @@ GEM
rdiscount (2.0.7.1)
rdoc (3.12.2)
json (~> 1.4)
redcarpet (2.1.1)
redcarpet (2.2.2)
redis (3.0.3)
redis-namespace (1.2.1)
redis (~> 3.0.0)
@ -370,6 +374,7 @@ GEM
builder
expression_parser
will_paginate (3.0.4)
yajl-ruby (1.1.0)
PLATFORMS
ruby
@ -391,7 +396,9 @@ DEPENDENCIES
devise (~> 2.1.2)
diff-display (~> 0.0.1)
factory_girl_rails (~> 4.0.0)
github-linguist (~> 2.2.1)
gemoji (~> 1.2.1)
github-linguist (~> 2.3.4)
gitlab-pygments.rb (~> 0.3.2)
gollum (~> 2.1.3)
grack!
grit!
@ -415,7 +422,7 @@ DEPENDENCIES
rails3-generators
rails3-jquery-autocomplete (~> 1.0.7)
rdiscount
redcarpet (~> 2.1.1)
redcarpet (~> 2.2.2)
redhillonrails_core!
resque (~> 1.21.0)
resque-status (~> 0.3.3)

View File

@ -38,14 +38,14 @@ module ApplicationHelper
end
end
def markdown(text)
unless @redcarpet
html_options = {filter_html: true, hard_wrap: true, with_toc_data: true}
options = {no_intraemphasis: true, tables: true, fenced_code_blocks: true, autolink: true, strikethrough: true, lax_html_blocks: true}
@redcarpet = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(html_options), options)
end
@redcarpet.render(text).html_safe
end
#~ def markdown(text)
#~ unless @redcarpet
#~ html_options = {filter_html: true, hard_wrap: true, with_toc_data: true}
#~ options = {no_intraemphasis: true, tables: true, fenced_code_blocks: true, autolink: true, strikethrough: true, lax_html_blocks: true}
#~ @redcarpet = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(html_options), options)
#~ end
#~ @redcarpet.render(text).html_safe
#~ end
def local_alert(text, type = 'error')
html = "<div class='flash'><div class='alert #{type}'> #{text}"

View File

@ -0,0 +1,62 @@
# This module is based on
# https://github.com/gitlabhq/gitlabhq/blob/7665b1de7eed4addd7b94786c84e6674710e6377/app/helpers/gitlab_markdown_helper.rb
module GitlabMarkdownHelper
include Modules::Models::Markdown
# Use this in places where you would normally use link_to(gfm(...), ...).
#
# It solves a problem occurring with nested links (i.e.
# "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be
# interpreted as intended. Browsers will parse something like
# "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is
# not linked any more). link_to_gfm corrects that. It wraps all parts to
# explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
escaped_body = if body =~ /^\<img/
body
else
escape_once(body)
end
gfm_body = gfm(escaped_body, html_options)
gfm_body.gsub!(%r{<a.*?>.*?</a>}m) do |match|
"</a>#{match}#{link_to("", url, html_options)[0..-5]}" # "</a>".length +1
end
link_to(gfm_body.html_safe, url, html_options)
end
def markdown(text)
unless @markdown
gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self,
# see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
filter_html: true,
with_toc_data: true,
hard_wrap: true)
@markdown = Redcarpet::Markdown.new(gitlab_renderer,
# see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
no_intra_emphasis: true,
tables: true,
fenced_code_blocks: true,
autolink: true,
strikethrough: true,
lax_html_blocks: true,
space_after_headers: true,
superscript: true)
end
@markdown.render(text).html_safe
end
def render_wiki_content(wiki_page)
if wiki_page.format == :markdown
markdown(wiki_page.content)
else
wiki_page.formatted_content.html_safe
end
end
end

View File

@ -10,3 +10,6 @@ Rosa::Application.config.middleware.insert_after ::Rails::Rack::Logger, ::Grack:
Rosa::Application.config.middleware.insert_before ::Grack::Handler, ::Grack::Auth
Rosa::Application.config.action_mailer.default_url_options = { :host => APP_CONFIG['action_mailer_host'] } if APP_CONFIG['action_mailer_host']
# Workaround for https://github.com/github/gemoji/pull/18
Rosa::Application.config.assets.paths << Emoji.images_path

View File

@ -1,5 +1,7 @@
en:
activerecord:
models:
issue: Issue
attributes:
issue:
title: Name

View File

@ -1,5 +1,7 @@
ru:
activerecord:
models:
issue: Задача
attributes:
issue:
title: Название

View File

@ -45,6 +45,8 @@ en:
save_error: Unable to save pull request
activerecord:
models:
pull_request: Pull Request
attributes:
pull_requests:
to_ref: Source

View File

@ -46,6 +46,8 @@ ru:
save_error: Не удалось сохранить пул реквест
activerecord:
models:
pull_request: Пул реквест
attributes:
pull_requests:
to_ref: Приемник

View File

@ -0,0 +1,192 @@
# This module is based on
# https://github.com/gitlabhq/gitlabhq/blob/397c3da9758c03a215a308c011f94261d9c61cfa/lib/gitlab/markdown.rb
module Modules
module Models
# Custom parser for GitLab-flavored Markdown
#
# It replaces references in the text with links to the appropriate items in
# GitLab.
#
# Supported reference formats are:
# * @foo for team members
# * #123 for issues
# * !123 for pull requests
# * 123456 for commits
#
# It also parses Emoji codes to insert images. See
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
#
# Examples
#
# >> gfm("Hey @david, can you fix this?")
# => "Hey <a href="/users/david">@david</a>, can you fix this?"
#
# >> gfm("Commit 35d5f7c closes #1234")
# => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>"
#
# >> gfm(":trollface:")
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
module Markdown
include IssuesHelper
attr_reader :html_options
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
# html_options - extra options for the reference links as given to link_to
#
# Note: reference links will only be generated if @project is set
def gfm(text, html_options = {})
return text if text.nil?
# Duplicate the string so we don't alter the original, then call to_str
# to cast it back to a String instead of a SafeBuffer. This is required
# for gsub calls to work as we need them to.
text = text.dup.to_str
@html_options = html_options
# Extract pre blocks so they are not altered
# from http://github.github.com/github-flavored-markdown/
text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) }
# Extract links with probably parsable hrefs
text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) }
# Extract images with probably parsable src
text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) }
# TODO: add popups with additional information
text = parse(text)
# Insert pre block extractions
text.gsub!(/\{gfm-extraction-(\h{32})\}/) do
insert_piece($1)
end
sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class)
end
private
def extract_piece(text)
@extractions ||= {}
md5 = Digest::MD5.hexdigest(text)
@extractions[md5] = text
"{gfm-extraction-#{md5}}"
end
def insert_piece(id)
@extractions[id]
end
# Private: Parses text for references and emoji
#
# text - Text to parse
#
# Note: reference links will only be generated if @project is set
#
# Returns parsed text
def parse(text)
parse_references(text) if @project
parse_emoji(text)
text
end
REFERENCE_PATTERN = %r{
(?<prefix>\W)? # Prefix
( # Reference
@(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User uname
|\#(?<issue>\d+) # Issue ID
|!(?<pull_request>\d+) # PR ID
|(?<commit>[\h]{6,40}) # Commit ID
)
(?<suffix>\W)? # Suffix
}x.freeze
TYPES = [:user, :issue, :pull_request, :commit].freeze
def parse_references(text)
# parse reference links
text.gsub!(REFERENCE_PATTERN) do |match|
prefix = $~[:prefix]
suffix = $~[:suffix]
type = TYPES.select{|t| !$~[t].nil?}.first
identifier = $~[type]
# Avoid HTML entities
if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
match
elsif ref_link = reference_link(type, identifier)
"#{prefix}#{ref_link}#{suffix}"
else
match
end
end
end
EMOJI_PATTERN = %r{(:(\S+):)}.freeze
def parse_emoji(text)
# parse emoji
text.gsub!(EMOJI_PATTERN) do |match|
if valid_emoji?($2)
image_tag(image_path("emoji/#{$2}.png"), class: 'emoji', title: $1, alt: $1, size: "20x20")
else
match
end
end
end
# Private: Checks if an emoji icon exists in the image asset directory
#
# emoji - Identifier of the emoji as a string (e.g., "+1", "heart")
#
# Returns boolean
def valid_emoji?(emoji)
Emoji.names.include? emoji
end
# Private: Dispatches to a dedicated processing method based on reference
#
# reference - Object reference ("@1234", "!567", etc.)
# identifier - Object identifier (Issue ID, SHA hash, etc.)
#
# Returns string rendered by the processing method
def reference_link(type, identifier)
send("reference_#{type}", identifier)
end
def reference_user(identifier)
if member = @project.all_members.select {|u| u.uname == identifier} #.joins(:user).where(users: {uname: identifier}).first
link_to("@#{identifier}", user_path(identifier), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member
end
end
def reference_issue(identifier)
if issue = @project.issues.where(serial_id: identifier).first
url = project_issue_path(@project.owner, @project.name, issue.serial_id)
title = "#{Issue.model_name.human}: #{issue.title}"
link_to("##{identifier}", url, html_options.merge(title: title, class: "gfm gfm-issue #{html_options[:class]}"))
end
end
def reference_pull_request(identifier)
if pull_request = @project.pull_requests.includes(:issue).where(issues: {serial_id: identifier}).first
title = "#{PullRequest.model_name.human}: #{pull_request.title}"
link_to("!#{identifier}", project_pull_request_path(@project, pull_request), html_options.merge(title: title, class: "gfm gfm-pull_request #{html_options[:class]}"))
end
end
def reference_commit(identifier)
if commit = @project.repo.commit(identifier)
link_to shortest_hash_id(@commit.id), commit_path(options[:project], @commit.id)
title = GitPresenters::CommitAsMessagePresenter.present(commit, :project => @project).caption
link_to(identifier, commit_path(@project, commit), html_options.merge(title: title, class: "gfm gfm-commit #{html_options[:class]}"))
end
end
end
end
end

View File

@ -0,0 +1,39 @@
# This class is based on
# https://github.com/gitlabhq/gitlabhq/blob/2bc78739a7aa9d7e5109281fc45dbd41a1a576d4/lib/gitlab/markdown.rb
class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
attr_reader :template
alias_method :h, :template
def initialize(template, options = {})
@template = template
@project = @template.instance_variable_get("@project")
super options
end
def block_code(code, language)
options = { options: {encoding: 'utf-8'} }
options.merge!(lexer: language.downcase) if Pygments::Lexer.find(language)
# New lines are placed to fix an rendering issue
# with code wrapped inside <h1> tag for next case:
#
# # Title kinda h1
#
# ruby code here
#
<<-HTML
<div>#{Pygments.highlight(code, options)}</div>
HTML
end
def link(link, title, content)
h.link_to_gfm(content, link, title: title)
end
def postprocess(full_document)
h.gfm(full_document)
end
end