From e78e73cae8daf1a6cdbdd27c48fdd5ea35690fa2 Mon Sep 17 00:00:00 2001 From: Alexander Machehin Date: Fri, 29 Mar 2013 00:22:24 +0600 Subject: [PATCH] [#19] add gitlabhq markdown --- Gemfile | 7 +- Gemfile.lock | 17 +- app/helpers/application_helper.rb | 16 +- app/helpers/gitlab_markdown_helper.rb | 62 +++++++ config/initializers/setup.rb | 3 + config/locales/models/issue.en.yml | 2 + config/locales/models/issue.ru.yml | 2 + config/locales/models/pull_request.en.yml | 2 + config/locales/models/pull_request.ru.yml | 2 + lib/modules/models/markdown.rb | 192 ++++++++++++++++++++++ lib/redcarpet/render/gitlab_html.rb | 39 +++++ 11 files changed, 329 insertions(+), 15 deletions(-) create mode 100644 app/helpers/gitlab_markdown_helper.rb create mode 100644 lib/modules/models/markdown.rb create mode 100644 lib/redcarpet/render/gitlab_html.rb diff --git a/Gemfile b/Gemfile index fb9224fac..c717eb7fb 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index c73f08a63..9d63ed2b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d2498da18..db535a7e2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 = "
#{text}" diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb new file mode 100644 index 000000000..6a9ad45e9 --- /dev/null +++ b/app/helpers/gitlab_markdown_helper.rb @@ -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. + # "outer text gfm ref more outer text"). This will not be + # interpreted as intended. Browsers will parse something like + # "outer text gfm ref 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. + # "outer text gfm ref more outer text"). + def link_to_gfm(body, url, html_options = {}) + return "" if body.blank? + + escaped_body = if body =~ /^\.*?}m) do |match| + "#{match}#{link_to("", url, html_options)[0..-5]}" # "".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 diff --git a/config/initializers/setup.rb b/config/initializers/setup.rb index bb9d9b5c0..39f5bf3f3 100644 --- a/config/initializers/setup.rb +++ b/config/initializers/setup.rb @@ -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 \ No newline at end of file diff --git a/config/locales/models/issue.en.yml b/config/locales/models/issue.en.yml index c06dfd908..b9930d4e2 100644 --- a/config/locales/models/issue.en.yml +++ b/config/locales/models/issue.en.yml @@ -1,5 +1,7 @@ en: activerecord: + models: + issue: Issue attributes: issue: title: Name diff --git a/config/locales/models/issue.ru.yml b/config/locales/models/issue.ru.yml index 1e1e3c190..51bd8db5e 100644 --- a/config/locales/models/issue.ru.yml +++ b/config/locales/models/issue.ru.yml @@ -1,5 +1,7 @@ ru: activerecord: + models: + issue: Задача attributes: issue: title: Название diff --git a/config/locales/models/pull_request.en.yml b/config/locales/models/pull_request.en.yml index 156bf8192..a27122c72 100644 --- a/config/locales/models/pull_request.en.yml +++ b/config/locales/models/pull_request.en.yml @@ -45,6 +45,8 @@ en: save_error: Unable to save pull request activerecord: + models: + pull_request: Pull Request attributes: pull_requests: to_ref: Source diff --git a/config/locales/models/pull_request.ru.yml b/config/locales/models/pull_request.ru.yml index 26dd0f761..e67132b06 100644 --- a/config/locales/models/pull_request.ru.yml +++ b/config/locales/models/pull_request.ru.yml @@ -46,6 +46,8 @@ ru: save_error: Не удалось сохранить пул реквест activerecord: + models: + pull_request: Пул реквест attributes: pull_requests: to_ref: Приемник diff --git a/lib/modules/models/markdown.rb b/lib/modules/models/markdown.rb new file mode 100644 index 000000000..743976da1 --- /dev/null +++ b/lib/modules/models/markdown.rb @@ -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 @david, can you fix this?" + # + # >> gfm("Commit 35d5f7c closes #1234") + # => "Commit 35d5f7c closes #1234" + # + # >> gfm(":trollface:") + # => "\":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{
.*?
|.*?}m) { |match| extract_piece(match) } + # Extract links with probably parsable hrefs + text.gsub!(%r{.*?}m) { |match| extract_piece(match) } + # Extract images with probably parsable src + text.gsub!(%r{}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{ + (?\W)? # Prefix + ( # Reference + @(?[a-zA-Z][a-zA-Z0-9_\-\.]*) # User uname + |\#(?\d+) # Issue ID + |!(?\d+) # PR ID + |(?[\h]{6,40}) # Commit ID + ) + (?\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 \ No newline at end of file diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb new file mode 100644 index 000000000..732d2ae7a --- /dev/null +++ b/lib/redcarpet/render/gitlab_html.rb @@ -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

tag for next case: + # + # # Title kinda h1 + # + # ruby code here + # + <<-HTML + +
#{Pygments.highlight(code, options)}
+ + 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