Decouple Gitlab::Markdown from the GitlabMarkdownHelper
This module is now the sole source of knowledge for *how* we render Markdown (and GFM).
This commit is contained in:
parent
10ee826847
commit
4340dd3eeb
|
@ -1,7 +1,6 @@
|
||||||
require 'nokogiri'
|
require 'nokogiri'
|
||||||
|
|
||||||
module GitlabMarkdownHelper
|
module GitlabMarkdownHelper
|
||||||
include Gitlab::Markdown
|
|
||||||
include PreferencesHelper
|
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(...), ...).
|
||||||
|
@ -22,7 +21,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'
|
||||||
|
@ -42,29 +41,16 @@ module GitlabMarkdownHelper
|
||||||
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,
|
project: @project,
|
||||||
strikethrough: true,
|
project_wiki: @project_wiki,
|
||||||
lax_spacing: true,
|
ref: @ref,
|
||||||
space_after_headers: true,
|
requested_path: @path
|
||||||
superscript: true,
|
)
|
||||||
footnotes: true
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def markdown(text, options={})
|
Gitlab::Markdown.render(text, context)
|
||||||
unless @markdown && options == @options
|
|
||||||
@options = options
|
|
||||||
|
|
||||||
# see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch
|
|
||||||
rend = Redcarpet::Render::GitlabHTML.new(self, options)
|
|
||||||
|
|
||||||
# see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
|
|
||||||
@markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS)
|
|
||||||
end
|
|
||||||
|
|
||||||
@markdown.render(text).html_safe
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def asciidoc(text)
|
def asciidoc(text)
|
||||||
|
|
|
@ -5,6 +5,44 @@ 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
|
||||||
|
# https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
|
||||||
|
REDCARPET_OPTIONS = {
|
||||||
|
no_intra_emphasis: true,
|
||||||
|
tables: true,
|
||||||
|
fenced_code_blocks: true,
|
||||||
|
strikethrough: true,
|
||||||
|
lax_spacing: true,
|
||||||
|
space_after_headers: true,
|
||||||
|
superscript: true,
|
||||||
|
footnotes: true
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
self.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 +56,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'
|
||||||
|
@ -29,7 +68,7 @@ module Gitlab
|
||||||
# :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
|
# html_options - extra options for the reference links as given to link_to
|
||||||
def gfm(text, options = {}, html_options = {})
|
def self.gfm(text, 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 +79,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 +100,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 +121,22 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def self.renderer
|
||||||
|
@markdown ||= begin
|
||||||
|
renderer = Redcarpet::Render::HTML.new
|
||||||
|
Redcarpet::Markdown.new(renderer, REDCARPET_OPTIONS)
|
||||||
|
end
|
||||||
|
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,
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
|
@ -10,7 +10,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 +21,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 +38,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,
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
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
|
|
|
@ -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,10 +19,10 @@ describe GitlabMarkdownHelper do
|
||||||
@project = project
|
@project = project
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#gfm" do
|
describe "#markdown" do
|
||||||
it "should forward HTML options to links" do
|
it "should forward HTML options to links" do
|
||||||
expect(gfm("Fixed in #{commit.id}", { project: @project }, class: 'foo')).
|
expect(markdown("Fixed in #{commit.id}", project: @project)).
|
||||||
to have_selector('a.gfm.foo')
|
to have_selector('a.gfm')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "referencing multiple objects" do
|
describe "referencing multiple objects" do
|
||||||
|
@ -30,17 +30,17 @@ describe GitlabMarkdownHelper do
|
||||||
|
|
||||||
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
|
||||||
|
@ -79,16 +79,6 @@ describe GitlabMarkdownHelper do
|
||||||
expect(doc.css('a')[4].text).to eq ' for real'
|
expect(doc.css('a')[4].text).to eq ' for real'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should forward HTML options' do
|
|
||||||
actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
|
|
||||||
doc = Nokogiri::HTML.parse(actual)
|
|
||||||
|
|
||||||
expect(doc.css('a')).to satisfy do |v|
|
|
||||||
# 'foo' gets added to all links
|
|
||||||
v.all? { |a| a.attr('class').match(/foo$/) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "escapes HTML passed in as the body" do
|
it "escapes HTML passed in as the body" do
|
||||||
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
|
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
|
||||||
expect(link_to_gfm(actual, commit_path)).
|
expect(link_to_gfm(actual, commit_path)).
|
||||||
|
|
Loading…
Reference in New Issue