Commit fb0a0d47 authored by Pavel Shutsin's avatar Pavel Shutsin

Merge branch...

Merge branch '345744-feature-flag-enable-cmark-commonmark-renderer-use_cmark_renderer' into 'master'

[Feature flag] Enable cmark CommonMark renderer (`:use_cmark_renderer`)

See merge request gitlab-org/gitlab!75800
parents 89108e00 cc238077
---
name: use_cmark_renderer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61792
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345744
milestone: '14.6'
type: development
group: group::project management
default_enabled: true
...@@ -7,13 +7,14 @@ module Banzai ...@@ -7,13 +7,14 @@ module Banzai
# Footnotes are supported in CommonMark. However we were stripping # Footnotes are supported in CommonMark. However we were stripping
# the ids during sanitization. Those are now allowed. # the ids during sanitization. Those are now allowed.
# #
# Footnotes are numbered the same - the first one has `id=fn1`, the # Footnotes are numbered as an increasing integer starting at `1`.
# second is `id=fn2`, etc. In order to allow footnotes when rendering # The `id` associated with a footnote is based on the footnote reference
# multiple markdown blocks on a page, we need to make each footnote # string. For example, `[^foot]` will generate `id="fn-foot"`.
# reference unique. # In order to allow footnotes when rendering multiple markdown blocks
# # on a page, we need to make each footnote reference unique.
# This filter adds a random number to each footnote (the same number # This filter adds a random number to each footnote (the same number
# can be used for a single render). So you get `id=fn1-4335` and `id=fn2-4335`. # can be used for a single render). So you get `id=fn-1-4335` and `id=fn-foot-4335`.
# #
class FootnoteFilter < HTML::Pipeline::Filter class FootnoteFilter < HTML::Pipeline::Filter
FOOTNOTE_ID_PREFIX = 'fn-' FOOTNOTE_ID_PREFIX = 'fn-'
...@@ -26,53 +27,24 @@ module Banzai ...@@ -26,53 +27,24 @@ module Banzai
CSS_FOOTNOTE = 'sup > a[data-footnote-ref]' CSS_FOOTNOTE = 'sup > a[data-footnote-ref]'
XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze
# only needed when feature flag use_cmark_renderer is turned off
INTEGER_PATTERN = /\A\d+\z/.freeze
FOOTNOTE_ID_PREFIX_OLD = 'fn'
FOOTNOTE_LINK_ID_PREFIX_OLD = 'fnref'
FOOTNOTE_LI_REFERENCE_PATTERN_OLD = /\A#{FOOTNOTE_ID_PREFIX_OLD}\d+\z/.freeze
FOOTNOTE_LINK_REFERENCE_PATTERN_OLD = /\A#{FOOTNOTE_LINK_ID_PREFIX_OLD}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1
CSS_SECTION_OLD = "ol > li[id=#{FOOTNOTE_ID_PREFIX_OLD}#{FOOTNOTE_START_NUMBER}]"
XPATH_SECTION_OLD = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION_OLD).freeze
def call def call
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) # Sanitization stripped off the section class - add it back in
# Sanitization stripped off the section class - add it back in return doc unless section_node = doc.at_xpath(XPATH_SECTION)
return doc unless section_node = doc.at_xpath(XPATH_SECTION)
section_node.append_class('footnotes') section_node.append_class('footnotes')
else
return doc unless first_footnote = doc.at_xpath(XPATH_SECTION_OLD)
return doc unless first_footnote.parent
first_footnote.parent.wrap('<section class="footnotes">')
end
rand_suffix = "-#{random_number}" rand_suffix = "-#{random_number}"
modified_footnotes = {} modified_footnotes = {}
xpath_footnote = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) doc.xpath(XPATH_FOOTNOTE).each do |link_node|
XPATH_FOOTNOTE ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
else ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
Gitlab::Utils::Nokogiri.css_to_xpath('sup > a[id]')
end
doc.xpath(xpath_footnote).each do |link_node|
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
else
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD)
end
css = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? "section[data-footnotes] li[id=#{fn_id(ref_num)}]" : "li[id=#{fn_id(ref_num)}]" css = "section[data-footnotes] li[id=#{fn_id(ref_num)}]"
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath(css) node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath(css)
footnote_node = doc.at_xpath(node_xpath) footnote_node = doc.at_xpath(node_xpath)
if footnote_node || modified_footnotes[ref_num] if footnote_node || modified_footnotes[ref_num]
next if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml) && !INTEGER_PATTERN.match?(ref_num)
link_node[:href] += rand_suffix link_node[:href] += rand_suffix
link_node[:id] += rand_suffix link_node[:id] += rand_suffix
...@@ -103,13 +75,11 @@ module Banzai ...@@ -103,13 +75,11 @@ module Banzai
end end
def fn_id(num) def fn_id(num)
prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_ID_PREFIX : FOOTNOTE_ID_PREFIX_OLD "#{FOOTNOTE_ID_PREFIX}#{num}"
"#{prefix}#{num}"
end end
def fnref_id(num) def fnref_id(num)
prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_LINK_ID_PREFIX : FOOTNOTE_LINK_ID_PREFIX_OLD "#{FOOTNOTE_LINK_ID_PREFIX}#{num}"
"#{prefix}#{num}"
end end
end end
end end
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
# This module is used in Banzai::Filter::MarkdownFilter. # This module is used in Banzai::Filter::MarkdownFilter.
# Used gem is `commonmarker` which is a ruby wrapper for libcmark (CommonMark parser) # Used gem is `commonmarker` which is a ruby wrapper for libcmark (CommonMark parser)
# including GitHub's GFM extensions. # including GitHub's GFM extensions.
# We now utilize the renderer built in `C`, rather than the ruby based renderer.
# Homepage: https://github.com/gjtorikian/commonmarker # Homepage: https://github.com/gjtorikian/commonmarker
module Banzai module Banzai
module Filter module Filter
module MarkdownEngines module MarkdownEngines
...@@ -22,57 +22,29 @@ module Banzai ...@@ -22,57 +22,29 @@ module Banzai
:VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD. :VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze ].freeze
RENDER_OPTIONS_C = [ RENDER_OPTIONS = [
:GITHUB_PRE_LANG, # use GitHub-style <pre lang> for fenced code blocks. :GITHUB_PRE_LANG, # use GitHub-style <pre lang> for fenced code blocks.
:FOOTNOTES, # render footnotes. :FOOTNOTES, # render footnotes.
:FULL_INFO_STRING, # include full info strings of code blocks in separate attribute. :FULL_INFO_STRING, # include full info strings of code blocks in separate attribute.
:UNSAFE # allow raw/custom HTML and unsafe links. :UNSAFE # allow raw/custom HTML and unsafe links.
].freeze ].freeze
# The `:GITHUB_PRE_LANG` option is not used intentionally because
# it renders a fence block with language as `<pre lang="LANG"><code>some code\n</code></pre>`
# while GitLab's syntax is `<pre><code lang="LANG">some code\n</code></pre>`.
# If in the future the syntax is about to be made GitHub-compatible, please, add `:GITHUB_PRE_LANG` render option below
# and remove `code_block` method from `lib/banzai/renderer/common_mark/html.rb`.
RENDER_OPTIONS_RUBY = [
# as of commonmarker 0.18.0, we need to use :UNSAFE to get the same as the original :DEFAULT
# https://github.com/gjtorikian/commonmarker/pull/81
:UNSAFE # allow raw/custom HTML and unsafe links.
].freeze
def initialize(context) def initialize(context)
@context = context @context = context
@renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml)
end end
def render(text) def render(text)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) CommonMarker.render_html(text, render_options, EXTENSIONS)
CommonMarker.render_html(text, render_options, extensions)
else
doc = CommonMarker.render_doc(text, PARSE_OPTIONS, extensions)
@renderer.render(doc)
end
end end
private private
def extensions
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
EXTENSIONS
else
EXTENSIONS + [
:tagfilter # strips out several "unsafe" HTML tags from being used: https://github.github.com/gfm/#disallowed-raw-html-extension-
].freeze
end
end
def render_options def render_options
@context[:no_sourcepos] ? render_options_no_sourcepos : render_options_sourcepos @context[:no_sourcepos] ? render_options_no_sourcepos : render_options_sourcepos
end end
def render_options_no_sourcepos def render_options_no_sourcepos
Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? RENDER_OPTIONS_C : RENDER_OPTIONS_RUBY RENDER_OPTIONS
end end
def render_options_sourcepos def render_options_sourcepos
......
...@@ -8,8 +8,10 @@ module Banzai ...@@ -8,8 +8,10 @@ module Banzai
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
CSS_A = 'a' CSS_A = 'a'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_LANG_TAG = 'pre'
XPATH_LANG_TAG = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_LANG_TAG).freeze
def call def call
return doc unless result[:escaped_literals] return doc unless result[:escaped_literals]
...@@ -32,22 +34,12 @@ module Banzai ...@@ -32,22 +34,12 @@ module Banzai
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title'] node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end end
doc.xpath(lang_tag).each do |node| doc.xpath(XPATH_LANG_TAG).each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang'] node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end end
doc doc
end end
private
def lang_tag
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Gitlab::Utils::Nokogiri.css_to_xpath('pre')
else
Gitlab::Utils::Nokogiri.css_to_xpath('code')
end
end
end end
end end
end end
...@@ -25,12 +25,7 @@ module Banzai ...@@ -25,12 +25,7 @@ module Banzai
private private
def lang_tag def lang_tag
@lang_tag ||= @lang_tag ||= Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
else
Gitlab::Utils::Nokogiri.css_to_xpath('pre > code[lang="plantuml"]').freeze
end
end end
def settings def settings
......
...@@ -28,12 +28,10 @@ module Banzai ...@@ -28,12 +28,10 @@ module Banzai
allowlist[:attributes]['li'] = %w[id] allowlist[:attributes]['li'] = %w[id]
allowlist[:transformers].push(self.class.remove_non_footnote_ids) allowlist[:transformers].push(self.class.remove_non_footnote_ids)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) # Allow section elements with data-footnotes attribute
# Allow section elements with data-footnotes attribute allowlist[:elements].push('section')
allowlist[:elements].push('section') allowlist[:attributes]['section'] = %w(data-footnotes)
allowlist[:attributes]['section'] = %w(data-footnotes) allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
end
allowlist allowlist
end end
...@@ -61,13 +59,8 @@ module Banzai ...@@ -61,13 +59,8 @@ module Banzai
return unless node.name == 'a' || node.name == 'li' return unless node.name == 'a' || node.name == 'li'
return unless node.has_attribute?('id') return unless node.has_attribute?('id')
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN
return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
else
return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN_OLD
return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN_OLD
end
node.remove_attribute('id') node.remove_attribute('id')
end end
......
...@@ -70,11 +70,11 @@ module Banzai ...@@ -70,11 +70,11 @@ module Banzai
private private
def parse_lang_params(node) def parse_lang_params(node)
node = node.parent if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) node = node.parent
# Commonmarker's FULL_INFO_STRING render option works with the space delimiter. # Commonmarker's FULL_INFO_STRING render option works with the space delimiter.
# But the current behavior of GitLab's markdown renderer is different - it grabs everything as the single # But the current behavior of GitLab's markdown renderer is different - it grabs everything as the single
# line, including language and its options. To keep backward compatability, we have to parse the old format and # line, including language and its options. To keep backward compatibility, we have to parse the old format and
# merge with the new one. # merge with the new one.
# #
# Behaviors before separating language and its parameters: # Behaviors before separating language and its parameters:
...@@ -91,11 +91,7 @@ module Banzai ...@@ -91,11 +91,7 @@ module Banzai
return unless language return unless language
language, language_params = language.split(LANG_PARAMS_DELIMITER, 2) language, language_params = language.split(LANG_PARAMS_DELIMITER, 2)
language_params = [node.attr('data-meta'), language_params].compact.join(' ')
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
language_params = [node.attr('data-meta'), language_params].compact.join(' ')
end
formatted_language_params = format_language_params(language_params) formatted_language_params = format_language_params(language_params)
[language, formatted_language_params] [language, formatted_language_params]
......
# frozen_string_literal: true
# Remove this entire file when removing `use_cmark_renderer` feature flag and switching to the CMARK html renderer.
# https://gitlab.com/gitlab-org/gitlab/-/issues/345744
module Banzai
module Renderer
module CommonMark
class HTML < CommonMarker::HtmlRenderer
def code_block(node)
block do
out("<pre#{sourcepos(node)}><code")
out(' lang="', node.fence_info, '"') if node.fence_info.present?
out('>')
out(escape_html(node.string_content))
out('</code></pre>')
end
end
end
end
end
end
...@@ -7,11 +7,7 @@ module Gitlab ...@@ -7,11 +7,7 @@ module Gitlab
register_for 'gitlab-html-pipeline' register_for 'gitlab-html-pipeline'
def format(node, lang, opts) def format(node, lang, opts)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) %(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
%(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
else
%(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
end
end end
end end
end end
......
...@@ -56,52 +56,6 @@ RSpec.describe Banzai::Filter::FootnoteFilter do ...@@ -56,52 +56,6 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
it 'properly adds the necessary ids and classes' do it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote.strip expect(doc.to_html).to eq filtered_footnote.strip
end end
context 'using ruby-based HTML renderer' do
# first[^1] and second[^second]
# [^1]: one
# [^second]: two
let(:footnote) do
<<~EOF
<p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p>
<p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p>
<ol>
<li id="fn1">
<p>one <a href="#fnref1">↩</a></p>
</li>
<li id="fn2">
<p>two <a href="#fnref2">↩</a></p>
</li>
</ol>
EOF
end
let(:filtered_footnote) do
<<~EOF
<p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p>
<p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p>
<section class="footnotes"><ol>
<li id="fn1-#{identifier}">
<p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p>
</li>
<li id="fn2-#{identifier}">
<p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p>
</li>
</ol></section>
EOF
end
let(:doc) { filter(footnote) }
let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
before do
stub_feature_flags(use_cmark_renderer: false)
end
it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote
end
end
end end
context 'when detecting footnotes' do context 'when detecting footnotes' do
......
...@@ -5,125 +5,90 @@ require 'spec_helper' ...@@ -5,125 +5,90 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::MarkdownFilter do RSpec.describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper include FilterSpecHelper
shared_examples_for 'renders correct markdown' do describe 'markdown engine from context' do
describe 'markdown engine from context' do it 'defaults to CommonMark' do
it 'defaults to CommonMark' do expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance| expect(instance).to receive(:render).and_return('test')
expect(instance).to receive(:render).and_return('test')
end
filter('test')
end end
it 'uses CommonMark' do filter('test')
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance| end
expect(instance).to receive(:render).and_return('test')
end
filter('test', { markdown_engine: :common_mark }) it 'uses CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect(instance).to receive(:render).and_return('test')
end end
filter('test', { markdown_engine: :common_mark })
end end
end
describe 'code block' do describe 'code block' do
context 'using CommonMark' do context 'using CommonMark' do
before do before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark) stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```", no_sourcepos: true)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="html"><code>')
else
expect(result).to start_with('<pre><code lang="html">')
end
end
it 'does not add language to lang attribute when not specified' do
result = filter("```\nsome code\n```", no_sourcepos: true)
expect(result).to start_with('<pre><code>')
end
it 'works with utf8 chars in language' do
result = filter("```日\nsome code\n```", no_sourcepos: true)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="日"><code>')
else
expect(result).to start_with('<pre><code lang="日">')
end
end
it 'works with additional language parameters' do
result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
else
expect(result).to start_with('<pre><code lang="ruby:red gem foo">')
end
end
end end
end
describe 'source line position' do it 'adds language to lang attribute when specified' do
context 'using CommonMark' do result = filter("```html\nsome code\n```", no_sourcepos: true)
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
it 'defaults to add data-sourcepos' do expect(result).to start_with('<pre lang="html"><code>')
result = filter('test') end
expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>' it 'does not add language to lang attribute when not specified' do
end result = filter("```\nsome code\n```", no_sourcepos: true)
it 'disables data-sourcepos' do expect(result).to start_with('<pre><code>')
result = filter('test', no_sourcepos: true) end
it 'works with utf8 chars in language' do
result = filter("```日\nsome code\n```", no_sourcepos: true)
expect(result).to eq '<p>test</p>' expect(result).to start_with('<pre lang="日"><code>')
end end
it 'works with additional language parameters' do
result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
end end
end end
end
describe 'footnotes in tables' do describe 'source line position' do
it 'processes footnotes in table cells' do context 'using CommonMark' do
text = <<-MD.strip_heredoc before do
| Column1 | stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
| --------- | end
| foot [^1] |
[^1]: a footnote it 'defaults to add data-sourcepos' do
MD result = filter('test')
result = filter(text, no_sourcepos: true) expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
end
expect(result).to include('<td>foot <sup') it 'disables data-sourcepos' do
result = filter('test', no_sourcepos: true)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) expect(result).to eq '<p>test</p>'
expect(result).to include('<section class="footnotes" data-footnotes>')
else
expect(result).to include('<section class="footnotes">')
end
end end
end end
end end
context 'using ruby-based HTML renderer' do describe 'footnotes in tables' do
before do it 'processes footnotes in table cells' do
stub_feature_flags(use_cmark_renderer: false) text = <<-MD.strip_heredoc
end | Column1 |
| --------- |
| foot [^1] |
it_behaves_like 'renders correct markdown' [^1]: a footnote
end MD
context 'using c-based HTML renderer' do result = filter(text, no_sourcepos: true)
before do
stub_feature_flags(use_cmark_renderer: true)
end
it_behaves_like 'renders correct markdown' expect(result).to include('<td>foot <sup')
expect(result).to include('<section class="footnotes" data-footnotes>')
end
end end
end end
...@@ -5,67 +5,33 @@ require 'spec_helper' ...@@ -5,67 +5,33 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::PlantumlFilter do RSpec.describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper include FilterSpecHelper
shared_examples_for 'renders correct markdown' do it 'replaces plantuml pre tag with img tag' do
it 'replaces plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
else doc = filter(input)
'<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
end
output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' expect(doc.to_s).to eq output
doc = filter(input) end
expect(doc.to_s).to eq output
end
it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
else
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
end
doc = filter(input)
expect(doc.to_s).to eq output
end
it 'does not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) it 'does not replace plantuml pre tag with img tag if disabled' do
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' stub_application_setting(plantuml_enabled: false)
else
'<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
end
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
doc = filter(input) output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq output expect(doc.to_s).to eq output
end
end end
context 'using ruby-based HTML renderer' do it 'does not replace plantuml pre tag with img tag if url is invalid' do
before do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
stub_feature_flags(use_cmark_renderer: false)
end
it_behaves_like 'renders correct markdown'
end
context 'using c-based HTML renderer' do input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
before do output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
stub_feature_flags(use_cmark_renderer: true) doc = filter(input)
end
it_behaves_like 'renders correct markdown' expect(doc.to_s).to eq output
end end
end end
...@@ -177,53 +177,6 @@ RSpec.describe Banzai::Filter::SanitizationFilter do ...@@ -177,53 +177,6 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
expect(act.to_html).to eq exp expect(act.to_html).to eq exp
end end
end end
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
it 'allows correct footnote id property on links' do
exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'allows correct footnote id property on li element' do
exp = %q(<ol><li id="fn1">footnote</li></ol>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'removes invalid id for footnote links' do
exp = %q(<a href="#fn1">link</a>)
%w[fnrefx test xfnref1].each do |id|
act = filter(%(<a href="#fn1" id="#{id}">link</a>))
expect(act.to_html).to eq exp
end
end
it 'removes invalid id for footnote li' do
exp = %q(<ol><li>footnote</li></ol>)
%w[fnx test xfn1].each do |id|
act = filter(%(<ol><li id="#{id}">footnote</li></ol>))
expect(act.to_html).to eq exp
end
end
it 'allows footnotes numbered higher than 9' do
exp = %q(<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>)
act = filter(exp)
expect(act.to_html).to eq exp
end
end
end end
end end
end end
...@@ -19,202 +19,142 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -19,202 +19,142 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end end
end end
shared_examples_for 'renders correct markdown' do context "when no language is specified" do
context "when no language is specified" do it "highlights as plaintext" do
it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>')
result = filter('<pre><code>def fun end</code></pre>')
expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>') expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>')
end
include_examples "XSS prevention", ""
end end
context "when contains mermaid diagrams" do include_examples "XSS prevention", ""
it "ignores mermaid blocks" do end
result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>') context "when contains mermaid diagrams" do
end it "ignores mermaid blocks" do
end result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
context "when a valid language is specified" do expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
it "highlights as that language" do end
result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) end
filter('<pre lang="ruby"><code>def fun end</code></pre>')
else
filter('<pre><code lang="ruby">def fun end</code></pre>')
end
expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>') context "when a valid language is specified" do
end it "highlights as that language" do
result = filter('<pre lang="ruby"><code>def fun end</code></pre>')
include_examples "XSS prevention", "ruby" expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>')
end end
context "when an invalid language is specified" do include_examples "XSS prevention", "ruby"
it "highlights as plaintext" do end
result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
else
filter('<pre><code lang="gnuplot">This is a test</code></pre>')
end
expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') context "when an invalid language is specified" do
end it "highlights as plaintext" do
result = filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
include_examples "XSS prevention", "gnuplot" expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
end end
context "languages that should be passed through" do include_examples "XSS prevention", "gnuplot"
let(:delimiter) { described_class::LANG_PARAMS_DELIMITER } end
let(:data_attr) { described_class::LANG_PARAMS_ATTR }
%w(math mermaid plantuml suggestion).each do |lang| context "languages that should be passed through" do
context "when #{lang} is specified" do let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
it "highlights as plaintext but with the correct language attribute and class" do let(:data_attr) { described_class::LANG_PARAMS_ATTR }
result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
else
filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
end
expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}) %w(math mermaid plantuml suggestion).each do |lang|
end context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
include_examples "XSS prevention", lang expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end end
context "when #{lang} has extra params" do include_examples "XSS prevention", lang
let(:lang_params) { 'foo-bar-kux' }
let(:xss_lang) do
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
"#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
"#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
end
end
it "includes data-lang-params tag with extra information" do
result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
else
filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
end
expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
include_examples "XSS prevention", lang
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
include_examples "XSS prevention",
"#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
include_examples "XSS prevention",
"#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
end
include_examples "XSS prevention",
"#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>"
end
end end
context 'when multiple param delimiters are used' do context "when #{lang} has extra params" do
let(:lang) { 'suggestion' } let(:lang_params) { 'foo-bar-kux' }
let(:lang_params) { '-1+10' } let(:xss_lang) { "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;" }
let(:expected_result) do
%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
end
context 'when delimiter is space' do
it 'delimits on the first appearance' do
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
expect(result.to_html.delete("\n")).to eq(expected_result) it "includes data-lang-params tag with extra information" do
else result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}) expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
end
end end
context 'when delimiter is colon' do include_examples "XSS prevention", lang
it 'delimits on the first appearance' do
result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) include_examples "XSS prevention",
expect(result.to_html.delete("\n")).to eq(expected_result) "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">This is a test</span></code></pre><copy-code></copy-code></div>}) include_examples "XSS prevention",
end "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>"
end
end
end end
end end
context "when sourcepos metadata is available" do context 'when multiple param delimiters are used' do
it "includes it in the highlighted code block" do let(:lang) { 'suggestion' }
result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>') let(:lang_params) { '-1+10' }
expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') let(:expected_result) do
%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
end end
end
context "when Rouge lexing fails" do context 'when delimiter is space' do
before do it 'delimits on the first appearance' do
allow_next_instance_of(Rouge::Lexers::Ruby) do |instance| result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
expect(result.to_html.delete("\n")).to eq(expected_result)
end end
end end
it "highlights as plaintext" do context 'when delimiter is colon' do
result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) it 'delimits on the first appearance' do
filter('<pre lang="ruby"><code>This is a test</code></pre>') result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
else
filter('<pre><code lang="ruby">This is a test</code></pre>')
end
expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>') expect(result.to_html.delete("\n")).to eq(expected_result)
end
end end
include_examples "XSS prevention", "ruby"
end end
end
context "when Rouge lexing fails after a retry" do context "when sourcepos metadata is available" do
before do it "includes it in the highlighted code block" do
allow_next_instance_of(Rouge::Lexers::PlainText) do |instance| result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end
end
it "does not add highlighting classes" do expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
result = filter('<pre><code>This is a test</code></pre>') end
end
expect(result.to_html).to eq('<pre><code>This is a test</code></pre>') context "when Rouge lexing fails" do
before do
allow_next_instance_of(Rouge::Lexers::Ruby) do |instance|
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end end
end
it "highlights as plaintext" do
result = filter('<pre lang="ruby"><code>This is a test</code></pre>')
include_examples "XSS prevention", "ruby" expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>')
end end
include_examples "XSS prevention", "ruby"
end end
context 'using ruby-based HTML renderer' do context "when Rouge lexing fails after a retry" do
before do before do
stub_feature_flags(use_cmark_renderer: false) allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end
end end
it_behaves_like 'renders correct markdown' it "does not add highlighting classes" do
end result = filter('<pre><code>This is a test</code></pre>')
context 'using c-based HTML renderer' do expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
before do
stub_feature_flags(use_cmark_renderer: true)
end end
it_behaves_like 'renders correct markdown' include_examples "XSS prevention", "ruby"
end end
end end
...@@ -65,47 +65,6 @@ RSpec.describe Banzai::Pipeline::FullPipeline do ...@@ -65,47 +65,6 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote.strip expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote.strip
end end
context 'using ruby-based HTML renderer' do
let(:html) { described_class.to_html(footnote_markdown, project: project) }
let(:identifier) { html[/.*fnref1-(\d+).*/, 1] }
let(:footnote_markdown) do
<<~EOF
first[^1] and second[^second] and twenty[^twenty]
[^1]: one
[^second]: two
[^twenty]: twenty
EOF
end
let(:filtered_footnote) do
<<~EOF
<p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn3-#{identifier}" id="fnref3-#{identifier}">3</a></sup></p>
<section class="footnotes"><ol>
<li id="fn1-#{identifier}">
<p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
<li id="fn2-#{identifier}">
<p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
<li id="fn3-#{identifier}">
<p>twenty <a href="#fnref3-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
</ol></section>
EOF
end
before do
stub_feature_flags(use_cmark_renderer: false)
end
it 'properly adds the necessary ids and classes' do
stub_commonmark_sourcepos_disabled
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
end
end
end end
describe 'links are detected as malicious' do describe 'links are detected as malicious' do
......
...@@ -5,117 +5,93 @@ require 'spec_helper' ...@@ -5,117 +5,93 @@ require 'spec_helper'
RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
shared_examples_for 'renders correct markdown' do describe 'backslash escapes', :aggregate_failures do
describe 'CommonMark tests', :aggregate_failures do let_it_be(:project) { create(:project, :public) }
it 'converts all reference punctuation to literals' do let_it_be(:issue) { create(:issue, project: project) }
reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
markdown = reference_chars.split('').map {|char| char.prepend("\\") }.join
punctuation = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.split('')
punctuation = punctuation.delete_if {|char| char == '&' }
punctuation << '&amp;'
result = described_class.call(markdown, project: project)
output = result[:output].to_html
punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
expect(result[:escaped_literals]).to be_truthy
end
it 'ensure we handle all the GitLab reference characters', :eager_load do it 'converts all reference punctuation to literals' do
reference_chars = ObjectSpace.each_object(Class).map do |klass| reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
next unless klass.included_modules.include?(Referable) markdown = reference_chars.split('').map {|char| char.prepend("\\") }.join
next unless klass.respond_to?(:reference_prefix) punctuation = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.split('')
next unless klass.reference_prefix.length == 1 punctuation = punctuation.delete_if {|char| char == '&' }
punctuation << '&amp;'
klass.reference_prefix result = described_class.call(markdown, project: project)
end.compact output = result[:output].to_html
reference_chars.all? do |char| punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.include?(char) expect(result[:escaped_literals]).to be_truthy
end end
end
it 'does not convert non-reference punctuation to spans' do it 'ensure we handle all the GitLab reference characters', :eager_load do
markdown = %q(\"\'\*\+\,\-\.\/\:\;\<\=\>\?\[\]\_\`\{\|\}) + %q[\(\)\\\\] reference_chars = ObjectSpace.each_object(Class).map do |klass|
next unless klass.included_modules.include?(Referable)
next unless klass.respond_to?(:reference_prefix)
next unless klass.reference_prefix.length == 1
result = described_class.call(markdown, project: project) klass.reference_prefix
output = result[:output].to_html end.compact
expect(output).not_to include('<span>') reference_chars.all? do |char|
expect(result[:escaped_literals]).to be_falsey Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.include?(char)
end end
end
it 'does not convert other characters to literals' do it 'does not convert non-reference punctuation to spans' do
markdown = %q(\→\A\a\ \3\φ\«) markdown = %q(\"\'\*\+\,\-\.\/\:\;\<\=\>\?\[\]\_\`\{\|\}) + %q[\(\)\\\\]
expected = '\→\A\a\ \3\φ\«'
result = correct_html_included(markdown, expected)
expect(result[:escaped_literals]).to be_falsey
end
describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do result = described_class.call(markdown, project: project)
where(:markdown, :expected) do output = result[:output].to_html
%q(`` \@\! ``) | %q(<code>\@\!</code>)
%q( \@\!) | %Q(<code>\\@\\!\n</code>)
%Q(~~~\n\\@\\!\n~~~) | %Q(<code>\\@\\!\n</code>)
%q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
%q[<a href="/bar\@)">] | %q[<a href="/bar%5C@)">]
end
with_them do
it { correct_html_included(markdown, expected) }
end
end
describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do expect(output).not_to include('<span>')
let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) } expect(result[:escaped_literals]).to be_falsey
it 'renders correct html' do
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
else
correct_html_included(markdown, %Q(<code lang="foo@bar">foo\n</code>))
end
end
where(:markdown, :expected) do
%q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
%Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
end
with_them do
it { correct_html_included(markdown, expected) }
end
end
end end
end
describe 'backslash escapes' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
def correct_html_included(markdown, expected)
result = described_class.call(markdown, {})
expect(result[:output].to_html).to include(expected) it 'does not convert other characters to literals' do
markdown = %q(\→\A\a\ \3\φ\«)
expected = '\→\A\a\ \3\φ\«'
result result = correct_html_included(markdown, expected)
expect(result[:escaped_literals]).to be_falsey
end end
context 'using ruby-based HTML renderer' do describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do
before do where(:markdown, :expected) do
stub_feature_flags(use_cmark_renderer: false) %q(`` \@\! ``) | %q(<code>\@\!</code>)
%q( \@\!) | %Q(<code>\\@\\!\n</code>)
%Q(~~~\n\\@\\!\n~~~) | %Q(<code>\\@\\!\n</code>)
%q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
%q[<a href="/bar\@)">] | %q[<a href="/bar%5C@)">]
end end
it_behaves_like 'renders correct markdown' with_them do
it { correct_html_included(markdown, expected) }
end
end end
context 'using c-based HTML renderer' do describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
before do let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
stub_feature_flags(use_cmark_renderer: true)
it 'renders correct html' do
correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
end
where(:markdown, :expected) do
%q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
%Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
end end
it_behaves_like 'renders correct markdown' with_them do
it { correct_html_included(markdown, expected) }
end
end end
end end
def correct_html_included(markdown, expected)
result = described_class.call(markdown, {})
expect(result[:output].to_html).to include(expected)
result
end
end end
...@@ -11,13 +11,27 @@ module Gitlab ...@@ -11,13 +11,27 @@ module Gitlab
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end end
shared_examples_for 'renders correct asciidoc' do context "without project" do
context "without project" do let(:input) { '<b>ascii</b>' }
let(:input) { '<b>ascii</b>' } let(:context) { {} }
let(:context) { {} } let(:html) { 'H<sub>2</sub>O' }
let(:html) { 'H<sub>2</sub>O' }
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
extensions: be_a(Proc)
}
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
expect(render(input, context)).to eq(html)
end
it "converts the input using Asciidoctor and default options" do context "with asciidoc_opts" do
it "merges the options with default ones" do
expected_asciidoc_opts = { expected_asciidoc_opts = {
safe: :secure, safe: :secure,
backend: :gitlab_html5, backend: :gitlab_html5,
...@@ -28,845 +42,808 @@ module Gitlab ...@@ -28,845 +42,808 @@ module Gitlab
expect(Asciidoctor).to receive(:convert) expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html) .with(input, expected_asciidoc_opts).and_return(html)
expect(render(input, context)).to eq(html) render(input, context)
end end
end
context "with asciidoc_opts" do context "with requested path" do
it "merges the options with default ones" do input = <<~ADOC
expected_asciidoc_opts = { Document name: {docname}.
safe: :secure, ADOC
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }), it "ignores {docname} when not available" do
extensions: be_a(Proc) expect(render(input, {})).to include(input.strip)
} end
[
['/', '', 'root'],
['README', 'README', 'just a filename'],
['doc/api/', '', 'a directory'],
['doc/api/README.adoc', 'README', 'a complete path']
].each do |path, basename, desc|
it "sets {docname} for #{desc}" do
expect(render(input, { requested_path: path })).to include(": #{basename}.")
end
end
end
expect(Asciidoctor).to receive(:convert) context "XSS" do
.with(input, expected_asciidoc_opts).and_return(html) items = {
'link with extra attribute' => {
input: 'link:mylink"onmouseover="alert(1)[Click Here]',
output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
},
'link with unsafe scheme' => {
input: 'link:data://danger[Click Here]',
output: "<div>\n<p><a>Click Here</a></p>\n</div>"
},
'image with onerror' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
}
}
render(input, context) items.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
expect(render(data[:input], context)).to include(data[:output])
end end
end end
context "with requested path" do # `stub_feature_flags method` runs AFTER declaration of `items` above.
# So the spec in its current implementation won't pass.
# Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
it "does not convert dangerous fenced code with inline script into HTML" do
input = '```mypre"><script>alert(3)</script>'
output = "<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>"
expect(render(input, context)).to include(output)
end
it 'does not allow locked attributes to be overridden' do
input = <<~ADOC input = <<~ADOC
Document name: {docname}. {counter:max-include-depth:1234}
<|-- {max-include-depth}
ADOC ADOC
it "ignores {docname} when not available" do expect(render(input, {})).not_to include('1234')
expect(render(input, {})).to include(input.strip) end
end end
[ context "images" do
['/', '', 'root'], it "does lazy load and link image" do
['README', 'README', 'just a filename'], input = 'image:https://localhost.com/image.png[]'
['doc/api/', '', 'a directory'], output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
['doc/api/README.adoc', 'README', 'a complete path'] expect(render(input, context)).to include(output)
].each do |path, basename, desc|
it "sets {docname} for #{desc}" do
expect(render(input, { requested_path: path })).to include(": #{basename}.")
end
end
end end
context "XSS" do it "does not automatically link image if link is explicitly defined" do
items = { input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
'link with extra attribute' => { output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
input: 'link:mylink"onmouseover="alert(1)[Click Here]', expect(render(input, context)).to include(output)
output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>" end
}, end
'link with unsafe scheme' => {
input: 'link:data://danger[Click Here]',
output: "<div>\n<p><a>Click Here</a></p>\n</div>"
},
'image with onerror' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
}
}
items.each do |name, data| context 'with admonition' do
it "does not convert dangerous #{name} into HTML" do it 'preserves classes' do
expect(render(data[:input], context)).to include(data[:output]) input = <<~ADOC
end NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
end ADOC
# `stub_feature_flags method` runs AFTER declaration of `items` above. output = <<~HTML
# So the spec in its current implementation won't pass. <div class="admonitionblock">
# Move this test back to the items hash when removing `use_cmark_renderer` feature flag. <table>
it "does not convert dangerous fenced code with inline script into HTML" do <tr>
input = '```mypre"><script>alert(3)</script>' <td class="icon">
output = <i class="fa icon-note" title="Note"></i>
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) </td>
"<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>" <td>
else An admonition paragraph, like this note, grabs the reader’s attention.
"<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>" </td>
end </tr>
</table>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
expect(render(input, context)).to include(output) context 'with passthrough' do
end it 'removes non heading ids' do
input = <<~ADOC
++++
<h2 id="foo">Title</h2>
++++
ADOC
it 'does not allow locked attributes to be overridden' do output = <<~HTML
input = <<~ADOC <h2>Title</h2>
{counter:max-include-depth:1234} HTML
<|-- {max-include-depth}
ADOC
expect(render(input, {})).not_to include('1234') expect(render(input, context)).to include(output.strip)
end
end end
context "images" do it 'removes non footnote def ids' do
it "does lazy load and link image" do input = <<~ADOC
input = 'image:https://localhost.com/image.png[]' ++++
output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" <div id="def">Footnote definition</div>
expect(render(input, context)).to include(output) ++++
end ADOC
it "does not automatically link image if link is explicitly defined" do output = <<~HTML
input = 'image:https://localhost.com/image.png[link=https://gitlab.com]' <div>Footnote definition</div>
output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" HTML
expect(render(input, context)).to include(output)
end expect(render(input, context)).to include(output.strip)
end end
context 'with admonition' do it 'removes non footnote ref ids' do
it 'preserves classes' do input = <<~ADOC
input = <<~ADOC ++++
NOTE: An admonition paragraph, like this note, grabs the reader’s attention. <a id="ref">Footnote reference</a>
ADOC ++++
ADOC
output = <<~HTML output = <<~HTML
<div class="admonitionblock"> <a>Footnote reference</a>
<table> HTML
<tr>
<td class="icon"> expect(render(input, context)).to include(output.strip)
<i class="fa icon-note" title="Note"></i>
</td>
<td>
An admonition paragraph, like this note, grabs the reader’s attention.
</td>
</tr>
</table>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with passthrough' do context 'with footnotes' do
it 'removes non heading ids' do it 'preserves ids and links' do
input = <<~ADOC input = <<~ADOC
++++ This paragraph has a footnote.footnote:[This is the text of the footnote.]
<h2 id="foo">Title</h2> ADOC
++++
ADOC
output = <<~HTML output = <<~HTML
<h2>Title</h2> <div>
HTML <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
</div>
<div>
<hr>
<div id="_footnotedef_1">
<a href="#_footnoteref_1">1</a>. This is the text of the footnote.
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
expect(render(input, context)).to include(output.strip) context 'with section anchors' do
end it 'preserves ids and links' do
input = <<~ADOC
= Title
it 'removes non footnote def ids' do == First section
input = <<~ADOC
++++
<div id="def">Footnote definition</div>
++++
ADOC
output = <<~HTML This is the first section.
<div>Footnote definition</div>
HTML
expect(render(input, context)).to include(output.strip) == Second section
end
it 'removes non footnote ref ids' do This is the second section.
input = <<~ADOC
++++
<a id="ref">Footnote reference</a>
++++
ADOC
output = <<~HTML == Thunder ⚡ !
<a>Footnote reference</a>
HTML
expect(render(input, context)).to include(output.strip) This is the third section.
end ADOC
output = <<~HTML
<h1>Title</h1>
<div>
<h2 id="user-content-first-section">
<a class="anchor" href="#user-content-first-section"></a>First section</h2>
<div>
<div>
<p>This is the first section.</p>
</div>
</div>
</div>
<div>
<h2 id="user-content-second-section">
<a class="anchor" href="#user-content-second-section"></a>Second section</h2>
<div>
<div>
<p>This is the second section.</p>
</div>
</div>
</div>
<div>
<h2 id="user-content-thunder">
<a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2>
<div>
<div>
<p>This is the third section.</p>
</div>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end end
end
context 'with footnotes' do context 'with xrefs' do
it 'preserves ids and links' do it 'preserves ids' do
input = <<~ADOC input = <<~ADOC
This paragraph has a footnote.footnote:[This is the text of the footnote.] Learn how to xref:cross-references[use cross references].
ADOC
output = <<~HTML [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
<div> ADOC
<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
</div> output = <<~HTML
<div> <div>
<hr> <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
<div id="_footnotedef_1"> </div>
<a href="#_footnoteref_1">1</a>. This is the text of the footnote. <div>
</div> <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
</div> </div>
HTML HTML
expect(render(input, context)).to include(output.strip) expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with section anchors' do context 'with checklist' do
it 'preserves ids and links' do it 'preserves classes' do
input = <<~ADOC input = <<~ADOC
= Title * [x] checked
* [ ] not checked
== First section ADOC
This is the first section.
== Second section
This is the second section.
== Thunder ⚡ !
This is the third section.
ADOC
output = <<~HTML output = <<~HTML
<h1>Title</h1> <div>
<div> <ul class="checklist">
<h2 id="user-content-first-section"> <li>
<a class="anchor" href="#user-content-first-section"></a>First section</h2> <p><i class="fa fa-check-square-o"></i> checked</p>
<div> </li>
<div> <li>
<p>This is the first section.</p> <p><i class="fa fa-square-o"></i> not checked</p>
</div> </li>
</div> </ul>
</div> </div>
<div> HTML
<h2 id="user-content-second-section">
<a class="anchor" href="#user-content-second-section"></a>Second section</h2> expect(render(input, context)).to include(output.strip)
<div>
<div>
<p>This is the second section.</p>
</div>
</div>
</div>
<div>
<h2 id="user-content-thunder">
<a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2>
<div>
<div>
<p>This is the third section.</p>
</div>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with xrefs' do context 'with marks' do
it 'preserves ids' do it 'preserves classes' do
input = <<~ADOC input = <<~ADOC
Learn how to xref:cross-references[use cross references]. Werewolves are allergic to #cassia cinnamon#.
[[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
ADOC
output = <<~HTML Did the werewolves read the [.small]#small print#?
<div>
<p>Learn how to <a href="#cross-references">use cross references</a>.</p>
</div>
<div>
<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
</div>
HTML
expect(render(input, context)).to include(output.strip) Where did all the [.underline.small]#cores# run off to?
end
We need [.line-through]#ten# make that twenty VMs.
[.big]##O##nce upon an infinite loop.
ADOC
output = <<~HTML
<div>
<p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p>
</div>
<div>
<p>Did the werewolves read the <span class="small">small print</span>?</p>
</div>
<div>
<p>Where did all the <span class="underline small">cores</span> run off to?</p>
</div>
<div>
<p>We need <span class="line-through">ten</span> make that twenty VMs.</p>
</div>
<div>
<p><span class="big">O</span>nce upon an infinite loop.</p>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end end
end
context 'with checklist' do context 'with fenced block' do
it 'preserves classes' do it 'highlights syntax' do
input = <<~ADOC input = <<~ADOC
* [x] checked ```js
* [ ] not checked console.log('hello world')
ADOC ```
ADOC
output = <<~HTML output = <<~HTML
<div> <div>
<ul class="checklist"> <div>
<li> <div class="gl-relative markdown-code-block js-markdown-code">
<p><i class="fa fa-check-square-o"></i> checked</p> <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
</li> <copy-code></copy-code>
<li> </div>
<p><i class="fa fa-square-o"></i> not checked</p> </div>
</li> </div>
</ul> HTML
</div>
HTML expect(render(input, context)).to include(output.strip)
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with marks' do context 'with listing block' do
it 'preserves classes' do it 'highlights syntax' do
input = <<~ADOC input = <<~ADOC
Werewolves are allergic to #cassia cinnamon#. [source,c++]
.class.cpp
Did the werewolves read the [.small]#small print#? ----
#include <stdio.h>
Where did all the [.underline.small]#cores# run off to?
We need [.line-through]#ten# make that twenty VMs.
[.big]##O##nce upon an infinite loop.
ADOC
output = <<~HTML for (int i = 0; i < 5; i++) {
<div> std::cout<<"*"<<std::endl;
<p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p> }
</div> ----
<div> ADOC
<p>Did the werewolves read the <span class="small">small print</span>?</p>
</div> output = <<~HTML
<div> <div>
<p>Where did all the <span class="underline small">cores</span> run off to?</p> <div>class.cpp</div>
</div> <div>
<div> <div class="gl-relative markdown-code-block js-markdown-code">
<p>We need <span class="line-through">ten</span> make that twenty VMs.</p> <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
</div> <span id="LC2" class="line" lang="cpp"></span>
<div> <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
<p><span class="big">O</span>nce upon an infinite loop.</p> <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
</div> <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
HTML <copy-code></copy-code>
</div>
expect(render(input, context)).to include(output.strip) </div>
end </div>
HTML
expect(render(input, context)).to include(output.strip)
end end
end
context 'with fenced block' do context 'with stem block' do
it 'highlights syntax' do it 'does not apply syntax highlighting' do
input = <<~ADOC input = <<~ADOC
```js [stem]
console.log('hello world') ++++
``` \sqrt{4} = 2
ADOC ++++
ADOC
output = <<~HTML output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
<div>
<div> expect(render(input, context)).to include(output)
<div class="gl-relative markdown-code-block js-markdown-code">
<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
<copy-code></copy-code>
</div>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with listing block' do context 'external links' do
it 'highlights syntax' do it 'adds the `rel` attribute to the link' do
input = <<~ADOC output = render('link:https://google.com[Google]', context)
[source,c++]
.class.cpp
----
#include <stdio.h>
for (int i = 0; i < 5; i++) {
std::cout<<"*"<<std::endl;
}
----
ADOC
output = <<~HTML expect(output).to include('rel="nofollow noreferrer noopener"')
<div>
<div>class.cpp</div>
<div>
<div class="gl-relative markdown-code-block js-markdown-code">
<pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
<span id="LC2" class="line" lang="cpp"></span>
<span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
<span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
<span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
<copy-code></copy-code>
</div>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with stem block' do context 'LaTex code' do
it 'does not apply syntax highlighting' do it 'adds class js-render-math to the output' do
input = <<~ADOC input = <<~MD
[stem] :stem: latexmath
++++
\sqrt{4} = 2
++++
ADOC
output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>" [stem]
++++
\sqrt{4} = 2
++++
expect(render(input, context)).to include(output) another part
end
[latexmath]
++++
\beta_x \gamma
++++
stem:[2+2] is 4
MD
expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
end end
end
context 'external links' do context 'outfilesuffix' do
it 'adds the `rel` attribute to the link' do it 'defaults to adoc' do
output = render('link:https://google.com[Google]', context) output = render("Inter-document reference <<README.adoc#>>", context)
expect(output).to include('rel="nofollow noreferrer noopener"') expect(output).to include("a href=\"README.adoc\"")
end
end end
end
context 'LaTex code' do context 'with mermaid diagrams' do
it 'adds class js-render-math to the output' do it 'adds class js-render-mermaid to the output' do
input = <<~MD input = <<~MD
:stem: latexmath [mermaid]
....
[stem] graph LR
++++ A[Square Rect] -- Link text --> B((Circle))
\sqrt{4} = 2 A --> C(Round Rect)
++++ B --> D{Rhombus}
C --> D
another part ....
MD
[latexmath]
++++ output = <<~HTML
\beta_x \gamma <pre data-mermaid-style="display" class="js-render-mermaid">graph LR
++++ A[Square Rect] -- Link text --&gt; B((Circle))
A --&gt; C(Round Rect)
stem:[2+2] is 4 B --&gt; D{Rhombus}
MD C --&gt; D</pre>
HTML
expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>') expect(render(input, context)).to include(output.strip)
end
end end
context 'outfilesuffix' do it 'applies subs in diagram block' do
it 'defaults to adoc' do input = <<~MD
output = render("Inter-document reference <<README.adoc#>>", context) :class-name: AveryLongClass
expect(output).to include("a href=\"README.adoc\"") [mermaid,subs=+attributes]
end ....
end classDiagram
Class01 <|-- {class-name} : Cool
....
MD
context 'with mermaid diagrams' do output = <<~HTML
it 'adds class js-render-mermaid to the output' do <pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
input = <<~MD Class01 &lt;|-- AveryLongClass : Cool</pre>
[mermaid] HTML
....
graph LR
A[Square Rect] -- Link text --> B((Circle))
A --> C(Round Rect)
B --> D{Rhombus}
C --> D
....
MD
output = <<~HTML
<pre data-mermaid-style="display" class="js-render-mermaid">graph LR
A[Square Rect] -- Link text --&gt; B((Circle))
A --&gt; C(Round Rect)
B --&gt; D{Rhombus}
C --&gt; D</pre>
HTML
expect(render(input, context)).to include(output.strip)
end
it 'applies subs in diagram block' do expect(render(input, context)).to include(output.strip)
input = <<~MD
:class-name: AveryLongClass
[mermaid,subs=+attributes]
....
classDiagram
Class01 <|-- {class-name} : Cool
....
MD
output = <<~HTML
<pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
Class01 &lt;|-- AveryLongClass : Cool</pre>
HTML
expect(render(input, context)).to include(output.strip)
end
end end
end
context 'with Kroki enabled' do context 'with Kroki enabled' do
before do before do
allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true) allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io') allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
end end
it 'converts a graphviz diagram to image' do
input = <<~ADOC
[graphviz]
....
digraph G {
Hello->World
}
....
ADOC
output = <<~HTML it 'converts a graphviz diagram to image' do
<div> input = <<~ADOC
<div> [graphviz]
<a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a> ....
</div> digraph G {
</div> Hello->World
HTML }
....
ADOC
expect(render(input, context)).to include(output.strip) output = <<~HTML
end <div>
<div>
<a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
</div>
</div>
HTML
it 'does not convert a blockdiag diagram to image' do expect(render(input, context)).to include(output.strip)
input = <<~ADOC end
[blockdiag]
....
blockdiag {
Kroki -> generates -> "Block diagrams";
Kroki -> is -> "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}
....
ADOC
output = <<~HTML it 'does not convert a blockdiag diagram to image' do
<div> input = <<~ADOC
<div> [blockdiag]
<pre>blockdiag { ....
Kroki -&gt; generates -&gt; "Block diagrams"; blockdiag {
Kroki -&gt; is -&gt; "very easy!"; Kroki -> generates -> "Block diagrams";
Kroki -> is -> "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"]; Kroki [color = "greenyellow"];
"very easy!" [color = "orange"]; "Block diagrams" [color = "pink"];
}</pre> "very easy!" [color = "orange"];
</div> }
</div> ....
HTML ADOC
expect(render(input, context)).to include(output.strip)
end
it 'does not allow kroki-plantuml-include to be overridden' do output = <<~HTML
input = <<~ADOC <div>
[plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"] <div>
.... <pre>blockdiag {
class BlockProcessor Kroki -&gt; generates -&gt; "Block diagrams";
Kroki -&gt; is -&gt; "very easy!";
BlockProcessor <|-- {counter:kroki-plantuml-include}
.... Kroki [color = "greenyellow"];
ADOC "Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}</pre>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
output = <<~HTML it 'does not allow kroki-plantuml-include to be overridden' do
<div> input = <<~ADOC
<div> [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
<a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a> ....
</div> class BlockProcessor
</div>
HTML
expect(render(input, {})).to include(output.strip) BlockProcessor <|-- {counter:kroki-plantuml-include}
end ....
ADOC
it 'does not allow kroki-server-url to be overridden' do output = <<~HTML
input = <<~ADOC <div>
[plantuml, test="{counter:kroki-server-url:evilsite}", format="png"] <div>
.... <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
class BlockProcessor </div>
</div>
BlockProcessor HTML
....
ADOC
expect(render(input, {})).not_to include('evilsite') expect(render(input, {})).to include(output.strip)
end
end end
context 'with Kroki and BlockDiag (additional format) enabled' do it 'does not allow kroki-server-url to be overridden' do
before do input = <<~ADOC
allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true) [plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io') ....
allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true) class BlockProcessor
end
it 'converts a blockdiag diagram to image' do
input = <<~ADOC
[blockdiag]
....
blockdiag {
Kroki -> generates -> "Block diagrams";
Kroki -> is -> "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}
....
ADOC
output = <<~HTML BlockProcessor
<div> ....
<div> ADOC
<a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip) expect(render(input, {})).not_to include('evilsite')
end
end end
end end
context 'with project' do context 'with Kroki and BlockDiag (additional format) enabled' do
let(:context) do before do
{ allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
commit: commit, allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
project: project, allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
ref: ref,
requested_path: requested_path
}
end end
let(:commit) { project.commit(ref) } it 'converts a blockdiag diagram to image' do
let(:project) { create(:project, :repository) } input = <<~ADOC
let(:ref) { 'asciidoc' } [blockdiag]
let(:requested_path) { '/' } ....
blockdiag {
Kroki -> generates -> "Block diagrams";
Kroki -> is -> "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}
....
ADOC
context 'include directive' do output = <<~HTML
subject(:output) { render(input, context) } <div>
<div>
<a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
</div>
</div>
HTML
let(:input) { "Include this:\n\ninclude::#{include_path}[]" } expect(render(input, context)).to include(output.strip)
end
end
end
before do context 'with project' do
current_file = requested_path let(:context) do
current_file += 'README.adoc' if requested_path.end_with? '/' {
commit: commit,
project: project,
ref: ref,
requested_path: requested_path
}
end
create_file(current_file, "= AsciiDoc\n") let(:commit) { project.commit(ref) }
end let(:project) { create(:project, :repository) }
let(:ref) { 'asciidoc' }
let(:requested_path) { '/' }
def many_includes(target) context 'include directive' do
Array.new(10, "include::#{target}[]").join("\n") subject(:output) { render(input, context) }
end
context 'cyclic imports' do let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
before do
create_file('doc/api/a.adoc', many_includes('b.adoc'))
create_file('doc/api/b.adoc', many_includes('a.adoc'))
end
let(:include_path) { 'a.adoc' } before do
let(:requested_path) { 'doc/api/README.md' } current_file = requested_path
current_file += 'README.adoc' if requested_path.end_with? '/'
it 'completes successfully' do create_file(current_file, "= AsciiDoc\n")
is_expected.to include('<p>Include this:</p>') end
end
def many_includes(target)
Array.new(10, "include::#{target}[]").join("\n")
end
context 'cyclic imports' do
before do
create_file('doc/api/a.adoc', many_includes('b.adoc'))
create_file('doc/api/b.adoc', many_includes('a.adoc'))
end end
context 'with path to non-existing file' do let(:include_path) { 'a.adoc' }
let(:include_path) { 'not-exists.adoc' } let(:requested_path) { 'doc/api/README.md' }
it 'renders Unresolved directive placeholder' do it 'completes successfully' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>") is_expected.to include('<p>Include this:</p>')
end
end end
end
shared_examples :invalid_include do context 'with path to non-existing file' do
let(:include_path) { 'dk.png' } let(:include_path) { 'not-exists.adoc' }
before do it 'renders Unresolved directive placeholder' do
allow(project.repository).to receive(:blob_at).and_return(blob) is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end end
end
it 'does not read the blob' do shared_examples :invalid_include do
expect(blob).not_to receive(:data) let(:include_path) { 'dk.png' }
end
it 'renders Unresolved directive placeholder' do before do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>") allow(project.repository).to receive(:blob_at).and_return(blob)
end
end end
context 'with path to a binary file' do it 'does not read the blob' do
let(:blob) { fake_blob(path: 'dk.png', binary: true) } expect(blob).not_to receive(:data)
end
include_examples :invalid_include it 'renders Unresolved directive placeholder' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end end
end
context 'with path to file in external storage' do context 'with path to a binary file' do
let(:blob) { fake_blob(path: 'dk.png', lfs: true) } let(:blob) { fake_blob(path: 'dk.png', binary: true) }
before do include_examples :invalid_include
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end
project.update_attribute(:lfs_enabled, true)
end
include_examples :invalid_include context 'with path to file in external storage' do
let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
project.update_attribute(:lfs_enabled, true)
end end
context 'with path to a textual file' do include_examples :invalid_include
let(:include_path) { 'sample.adoc' } end
before do context 'with path to a textual file' do
create_file(file_path, "Content from #{include_path}") let(:include_path) { 'sample.adoc' }
end
shared_examples :valid_include do before do
[ create_file(file_path, "Content from #{include_path}")
['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'], end
['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'], shared_examples :valid_include do
['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'], [
['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories'] ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
].each do |include_path_, file_path_, desc| ['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
context "the file is specified by #{desc}" do ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
let(:include_path) { include_path_ } ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'],
let(:file_path) { file_path_ } ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories']
].each do |include_path_, file_path_, desc|
it 'includes content of the file' do context "the file is specified by #{desc}" do
is_expected.to include('<p>Include this:</p>') let(:include_path) { include_path_ }
is_expected.to include("<p>Content from #{include_path}</p>") let(:file_path) { file_path_ }
end
it 'includes content of the file' do
is_expected.to include('<p>Include this:</p>')
is_expected.to include("<p>Content from #{include_path}</p>")
end end
end end
end end
end
context 'when requested path is a file in the repo' do context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.adoc' } let(:requested_path) { 'doc/api/README.adoc' }
include_examples :valid_include include_examples :valid_include
context 'without a commit (only ref)' do context 'without a commit (only ref)' do
let(:commit) { nil } let(:commit) { nil }
include_examples :valid_include include_examples :valid_include
end
end end
end
context 'when requested path is a directory in the repo' do context 'when requested path is a directory in the repo' do
let(:requested_path) { 'doc/api/' } let(:requested_path) { 'doc/api/' }
include_examples :valid_include include_examples :valid_include
context 'without a commit (only ref)' do context 'without a commit (only ref)' do
let(:commit) { nil } let(:commit) { nil }
include_examples :valid_include include_examples :valid_include
end
end end
end end
end
context 'when repository is passed into the context' do context 'when repository is passed into the context' do
let(:wiki_repo) { project.wiki.repository } let(:wiki_repo) { project.wiki.repository }
let(:include_path) { 'wiki_file.adoc' } let(:include_path) { 'wiki_file.adoc' }
before do
project.create_wiki
context.merge!(repository: wiki_repo)
end
context 'when the file exists' do
before do before do
project.create_wiki create_file(include_path, 'Content from wiki', repository: wiki_repo)
context.merge!(repository: wiki_repo)
end end
context 'when the file exists' do it { is_expected.to include('<p>Content from wiki</p>') }
before do end
create_file(include_path, 'Content from wiki', repository: wiki_repo)
end
it { is_expected.to include('<p>Content from wiki</p>') } context 'when the file does not exist' do
end it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
end
end
context 'when the file does not exist' do context 'recursive includes with relative paths' do
it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")} let(:input) do
end <<~ADOC
Source: requested file
include::doc/README.adoc[]
include::license.adoc[]
ADOC
end end
context 'recursive includes with relative paths' do before do
let(:input) do create_file 'doc/README.adoc', <<~ADOC
<<~ADOC Source: doc/README.adoc
Source: requested file
include::doc/README.adoc[]
include::license.adoc[]
ADOC
end
before do include::../license.adoc[]
create_file 'doc/README.adoc', <<~ADOC
Source: doc/README.adoc
include::../license.adoc[]
include::api/hello.adoc[]
ADOC
create_file 'license.adoc', <<~ADOC
Source: license.adoc
ADOC
create_file 'doc/api/hello.adoc', <<~ADOC
Source: doc/api/hello.adoc
include::./common.adoc[]
ADOC
create_file 'doc/api/common.adoc', <<~ADOC
Source: doc/api/common.adoc
ADOC
end
it 'includes content of the included files recursively' do include::api/hello.adoc[]
expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip ADOC
Source: requested file create_file 'license.adoc', <<~ADOC
Source: doc/README.adoc Source: license.adoc
Source: license.adoc ADOC
Source: doc/api/hello.adoc create_file 'doc/api/hello.adoc', <<~ADOC
Source: doc/api/common.adoc Source: doc/api/hello.adoc
Source: license.adoc
ADOC include::./common.adoc[]
end ADOC
create_file 'doc/api/common.adoc', <<~ADOC
Source: doc/api/common.adoc
ADOC
end end
def create_file(path, content, repository: project.repository) it 'includes content of the included files recursively' do
repository.create_file(project.creator, path, content, expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
message: "Add #{path}", branch_name: 'asciidoc') Source: requested file
Source: doc/README.adoc
Source: license.adoc
Source: doc/api/hello.adoc
Source: doc/api/common.adoc
Source: license.adoc
ADOC
end end
end end
end
end
context 'using ruby-based HTML renderer' do def create_file(path, content, repository: project.repository)
before do repository.create_file(project.creator, path, content,
stub_feature_flags(use_cmark_renderer: false) message: "Add #{path}", branch_name: 'asciidoc')
end end
it_behaves_like 'renders correct asciidoc'
end
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end end
it_behaves_like 'renders correct asciidoc'
end end
def render(*args) def render(*args)
......
...@@ -92,12 +92,7 @@ module StubGitlabCalls ...@@ -92,12 +92,7 @@ module StubGitlabCalls
end end
def stub_commonmark_sourcepos_disabled def stub_commonmark_sourcepos_disabled
render_options = render_options = Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS
if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C
else
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY
end
allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
.to receive(:render_options) .to receive(:render_options)
......
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