Commit afb2e6f4 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'rs-cleanup-redcarpet-renderer' into 'master'

Decouple Gitlab::Markdown from the GitlabMarkdownHelper

This module is now the sole source of knowledge for *how* we render
Markdown (and GFM).

See merge request !1223
parents 2236e9d6 1bcfe4d2
require 'nokogiri' require 'nokogiri'
module GitlabMarkdownHelper module GitlabMarkdownHelper
include Gitlab::Markdown
include PreferencesHelper
# Use this in places where you would normally use link_to(gfm(...), ...). # Use this in places where you would normally use link_to(gfm(...), ...).
# #
# It solves a problem occurring with nested links (i.e. # It solves a problem occurring with nested links (i.e.
...@@ -22,7 +19,7 @@ module GitlabMarkdownHelper ...@@ -22,7 +19,7 @@ module GitlabMarkdownHelper
escape_once(body) escape_once(body)
end end
gfm_body = gfm(escaped_body, {}, html_options) gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user)
fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -39,32 +36,38 @@ module GitlabMarkdownHelper ...@@ -39,32 +36,38 @@ module GitlabMarkdownHelper
end end
end end
# Add any custom CSS classes to the GFM-generated reference links
if html_options[:class]
fragment.css('a.gfm').add_class(html_options[:class])
end
fragment.to_html.html_safe fragment.to_html.html_safe
end end
MARKDOWN_OPTIONS = { def markdown(text, context = {})
no_intra_emphasis: true, context.merge!(
tables: true, current_user: current_user,
fenced_code_blocks: true, path: @path,
strikethrough: true, project: @project,
lax_spacing: true, project_wiki: @project_wiki,
space_after_headers: true, ref: @ref
superscript: true, )
footnotes: true
}.freeze Gitlab::Markdown.render(text, context)
end
def markdown(text, options={})
unless @markdown && options == @options # TODO (rspeicher): Remove all usages of this helper and just call `markdown`
@options = options # with a custom pipeline depending on the content being rendered
def gfm(text, options = {})
# see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch options.merge!(
rend = Redcarpet::Render::GitlabHTML.new(self, options) current_user: current_user,
path: @path,
# see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use project: @project,
@markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) project_wiki: @project_wiki,
end ref: @ref
)
@markdown.render(text).html_safe Gitlab::Markdown.gfm(text, options)
end end
def asciidoc(text) def asciidoc(text)
......
...@@ -5,6 +5,32 @@ module Gitlab ...@@ -5,6 +5,32 @@ module Gitlab
# #
# See the files in `lib/gitlab/markdown/` for specific processing information. # See the files in `lib/gitlab/markdown/` for specific processing information.
module Markdown module Markdown
# Convert a Markdown String into an HTML-safe String of HTML
#
# markdown - Markdown String
# context - Hash of context options passed to our HTML Pipeline
#
# Returns an HTML-safe String
def self.render(markdown, context = {})
html = renderer.render(markdown)
html = gfm(html, context)
html.html_safe
end
# Convert a Markdown String into HTML without going through the HTML
# Pipeline.
#
# Note that because the pipeline is skipped, SanitizationFilter is as well.
# Do not output the result of this method to the user.
#
# markdown - Markdown String
#
# Returns a String
def self.render_without_gfm(markdown)
renderer.render(markdown)
end
# Provide autoload paths for filters to prevent a circular dependency error # Provide autoload paths for filters to prevent a circular dependency error
autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
...@@ -18,6 +44,7 @@ module Gitlab ...@@ -18,6 +44,7 @@ module Gitlab
autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
autoload :SyntaxHighlightFilter, 'gitlab/markdown/syntax_highlight_filter'
autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :TaskListFilter, 'gitlab/markdown/task_list_filter'
autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
...@@ -28,8 +55,7 @@ module Gitlab ...@@ -28,8 +55,7 @@ module Gitlab
# options - A Hash of options used to customize output (default: {}): # options - A Hash of options used to customize output (default: {}):
# :xhtml - output XHTML instead of HTML # :xhtml - output XHTML instead of HTML
# :reference_only_path - Use relative path for reference links # :reference_only_path - Use relative path for reference links
# html_options - extra options for the reference links as given to link_to def self.gfm(text, options = {})
def gfm(text, options = {}, html_options = {})
return text if text.nil? return text if text.nil?
# Duplicate the string so we don't alter the original, then call to_str # Duplicate the string so we don't alter the original, then call to_str
...@@ -40,8 +66,8 @@ module Gitlab ...@@ -40,8 +66,8 @@ module Gitlab
options.reverse_merge!( options.reverse_merge!(
xhtml: false, xhtml: false,
reference_only_path: true, reference_only_path: true,
project: @project, project: options[:project],
current_user: current_user current_user: options[:current_user]
) )
@pipeline ||= HTML::Pipeline.new(filters) @pipeline ||= HTML::Pipeline.new(filters)
...@@ -61,12 +87,11 @@ module Gitlab ...@@ -61,12 +87,11 @@ module Gitlab
current_user: options[:current_user], current_user: options[:current_user],
only_path: options[:reference_only_path], only_path: options[:reference_only_path],
project: options[:project], project: options[:project],
reference_class: html_options[:class],
# RelativeLinkFilter # RelativeLinkFilter
ref: @ref, ref: options[:ref],
requested_path: @path, requested_path: options[:path],
project_wiki: @project_wiki project_wiki: options[:project_wiki]
} }
result = @pipeline.call(text, context) result = @pipeline.call(text, context)
...@@ -83,14 +108,36 @@ module Gitlab ...@@ -83,14 +108,36 @@ module Gitlab
private private
def self.renderer
@markdown ||= begin
renderer = Redcarpet::Render::HTML.new
Redcarpet::Markdown.new(renderer, redcarpet_options)
end
end
def self.redcarpet_options
# https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
@redcarpet_options ||= {
fenced_code_blocks: true,
footnotes: true,
lax_spacing: true,
no_intra_emphasis: true,
space_after_headers: true,
strikethrough: true,
superscript: true,
tables: true
}.freeze
end
# Filters used in our pipeline # Filters used in our pipeline
# #
# SanitizationFilter should come first so that all generated reference HTML # SanitizationFilter should come first so that all generated reference HTML
# goes through untouched. # goes through untouched.
# #
# See https://github.com/jch/html-pipeline#filters for more filters. # See https://github.com/jch/html-pipeline#filters for more filters.
def filters def self.filters
[ [
Gitlab::Markdown::SyntaxHighlightFilter,
Gitlab::Markdown::SanitizationFilter, Gitlab::Markdown::SanitizationFilter,
Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::RelativeLinkFilter,
......
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'uri' require 'uri'
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces commit range references with links. # HTML filter that replaces commit range references with links.
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces commit references with links. # HTML filter that replaces commit references with links.
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# Common methods for ReferenceFilters that support an optional cross-project # Common methods for ReferenceFilters that support an optional cross-project
......
require 'action_controller'
require 'gitlab/markdown'
require 'gitlab_emoji' require 'gitlab_emoji'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'action_controller'
module Gitlab module Gitlab
module Markdown module Markdown
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces external issue tracker references with links. # HTML filter that replaces external issue tracker references with links.
......
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Gitlab module Gitlab
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces issue references with links. References to # HTML filter that replaces issue references with links. References to
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces label references with links. # HTML filter that replaces label references with links.
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces merge request references with links. References # HTML filter that replaces merge request references with links. References
......
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Gitlab module Gitlab
...@@ -9,7 +10,6 @@ module Gitlab ...@@ -9,7 +10,6 @@ module Gitlab
# #
# Context options: # Context options:
# :project (required) - Current project, ignored if reference is cross-project. # :project (required) - Current project, ignored if reference is cross-project.
# :reference_class - Custom CSS class added to reference links.
# :only_path - Generate path-only links. # :only_path - Generate path-only links.
# #
# Results: # Results:
...@@ -70,7 +70,7 @@ module Gitlab ...@@ -70,7 +70,7 @@ module Gitlab
end end
def reference_class(type) def reference_class(type)
"gfm gfm-#{type} #{context[:reference_class]}".strip "gfm gfm-#{type}"
end end
# Iterate through the document's text nodes, yielding the current node's # Iterate through the document's text nodes, yielding the current node's
......
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'uri' require 'uri'
......
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter' require 'html/pipeline/sanitization_filter'
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces snippet references with links. References to # HTML filter that replaces snippet references with links. References to
......
require 'gitlab/markdown'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
module Gitlab
module Markdown
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
include Rouge::Plugins::Redcarpet
def call
doc.search('pre > code').each do |node|
highlight_node(node)
end
doc
end
def highlight_node(node)
language = node.attr('class')
code = node.text
highlighted = block_code(code, language)
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
private
# Override Rouge::Plugins::Redcarpet#rouge_formatter
def rouge_formatter(lexer)
Rouge::Formatters::HTMLGitlab.new(
cssclass: "code highlight js-syntax-highlight #{lexer.tag}")
end
end
end
end
require 'gitlab/markdown'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Gitlab module Gitlab
......
require 'gitlab/markdown'
require 'task_list/filter' require 'task_list/filter'
module Gitlab module Gitlab
......
require 'gitlab/markdown'
module Gitlab module Gitlab
module Markdown module Markdown
# HTML filter that replaces user or group references with links. # HTML filter that replaces user or group references with links.
......
require 'gitlab/markdown'
module Gitlab module Gitlab
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor class ReferenceExtractor
...@@ -10,7 +12,7 @@ module Gitlab ...@@ -10,7 +12,7 @@ module Gitlab
def analyze(text) def analyze(text)
references.clear references.clear
@text = markdown.render(text.dup) @text = Gitlab::Markdown.render_without_gfm(text)
end end
%i(user label issue merge_request snippet commit commit_range).each do |type| %i(user label issue merge_request snippet commit commit_range).each do |type|
...@@ -21,10 +23,6 @@ module Gitlab ...@@ -21,10 +23,6 @@ module Gitlab
private private
def markdown
@markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, GitlabMarkdownHelper::MARKDOWN_OPTIONS)
end
def references def references
@references ||= Hash.new do |references, type| @references ||= Hash.new do |references, type|
type = type.to_sym type = type.to_sym
...@@ -42,7 +40,7 @@ module Gitlab ...@@ -42,7 +40,7 @@ module Gitlab
# Returns the results Array for the requested filter type # Returns the results Array for the requested filter type
def pipeline_result(filter_type) def pipeline_result(filter_type)
klass = filter_type.to_s.camelize + 'ReferenceFilter' klass = filter_type.to_s.camelize + 'ReferenceFilter'
filter = "Gitlab::Markdown::#{klass}".constantize filter = Gitlab::Markdown.const_get(klass)
context = { context = {
project: project, project: project,
......
require 'active_support/core_ext/string/output_safety'
class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
attr_reader :template
alias_method :h, :template
def initialize(template, options = {})
@template = template
@options = options.dup
@options.reverse_merge!(
# Handled further down the line by Gitlab::Markdown::SanitizationFilter
escape_html: false,
project: @template.instance_variable_get("@project")
)
super(options)
end
def normal_text(text)
ERB::Util.html_escape_once(text)
end
# Stolen from Rouge::Plugins::Redcarpet as this module is not required
# from Rouge's gem root.
def block_code(code, language)
lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
# XXX HACK: Redcarpet strips hard tabs out of code blocks,
# so we assume you're not using leading spaces that aren't tabs,
# and just replace them here.
if lexer.tag == 'make'
code.gsub!(/^ /, "\t")
end
formatter = Rouge::Formatters::HTMLGitlab.new(
cssclass: "code highlight js-syntax-highlight #{lexer.tag}"
)
formatter.format(lexer.lex(code))
end
def postprocess(full_document)
h.gfm(full_document, @options)
end
end
...@@ -77,7 +77,7 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -77,7 +77,7 @@ describe "GitLab Flavored Markdown", feature: true do
it "should render details in issues#show" do it "should render details in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue) visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link("@#{fred.username}") expect(page).to have_link(fred.to_reference)
end end
end end
......
...@@ -179,7 +179,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -179,7 +179,7 @@ describe 'GitLab Markdown', feature: true do
before(:all) do before(:all) do
@feat = MarkdownFeature.new @feat = MarkdownFeature.new
# `gfm` helper depends on a `@project` variable # `markdown` helper expects a `@project` variable
@project = @feat.project @project = @feat.project
@html = markdown(@feat.raw_markdown) @html = markdown(@feat.raw_markdown)
......
...@@ -19,28 +19,23 @@ describe GitlabMarkdownHelper do ...@@ -19,28 +19,23 @@ describe GitlabMarkdownHelper do
@project = project @project = project
end end
describe "#gfm" do describe "#markdown" do
it "should forward HTML options to links" do
expect(gfm("Fixed in #{commit.id}", { project: @project }, class: 'foo')).
to have_selector('a.gfm.foo')
end
describe "referencing multiple objects" do describe "referencing multiple objects" do
let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" } let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
it "should link to the merge request" do it "should link to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request) expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(gfm(actual)).to match(expected) expect(markdown(actual)).to match(expected)
end end
it "should link to the commit" do it "should link to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit) expected = namespace_project_commit_path(project.namespace, project, commit)
expect(gfm(actual)).to match(expected) expect(markdown(actual)).to match(expected)
end end
it "should link to the issue" do it "should link to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue) expected = namespace_project_issue_path(project.namespace, project, issue)
expect(gfm(actual)).to match(expected) expect(markdown(actual)).to match(expected)
end end
end end
end end
......
...@@ -75,11 +75,6 @@ module Gitlab::Markdown ...@@ -75,11 +75,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
end end
it 'includes an optional custom class' do
doc = filter("See #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -71,11 +71,6 @@ module Gitlab::Markdown ...@@ -71,11 +71,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
end end
it 'includes an optional custom class' do
doc = filter("See #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -68,11 +68,6 @@ module Gitlab::Markdown ...@@ -68,11 +68,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end end
it 'includes an optional custom class' do
doc = filter("Issue #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do it 'supports an :only_path context' do
doc = filter("Issue #{reference}", only_path: true) doc = filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
......
...@@ -68,11 +68,6 @@ module Gitlab::Markdown ...@@ -68,11 +68,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end end
it 'includes an optional custom class' do
doc = filter("Issue #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("Issue #{reference}") doc = filter("Issue #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -25,11 +25,6 @@ module Gitlab::Markdown ...@@ -25,11 +25,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
end end
it 'includes an optional custom class' do
doc = filter("Label #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("Label #{reference}") doc = filter("Label #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -56,11 +56,6 @@ module Gitlab::Markdown ...@@ -56,11 +56,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
end end
it 'includes an optional custom class' do
doc = filter("Merge #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("Merge #{reference}") doc = filter("Merge #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -55,11 +55,6 @@ module Gitlab::Markdown ...@@ -55,11 +55,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
end end
it 'includes an optional custom class' do
doc = filter("Snippet #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'includes a data-project-id attribute' do it 'includes a data-project-id attribute' do
doc = filter("Snippet #{reference}") doc = filter("Snippet #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -130,11 +130,6 @@ module Gitlab::Markdown ...@@ -130,11 +130,6 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
end end
it 'includes an optional custom class' do
doc = filter("Hey #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do it 'supports an :only_path context' do
doc = filter("Hey #{reference}", only_path: true) doc = filter("Hey #{reference}", only_path: true)
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment