Commit de18ae10 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'blackst0ne/remove-banzai-html-renderer' into 'master'

Remove Banzai::Renderer::CommonMark::HTML renderer

See merge request gitlab-org/gitlab!61792
parents 9a04cff7 ad534355
---
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: false
......@@ -16,37 +16,60 @@ module Banzai
# can be used for a single render). So you get `id=fn1-4335` and `id=fn2-4335`.
#
class FootnoteFilter < HTML::Pipeline::Filter
INTEGER_PATTERN = /\A\d+\z/.freeze
FOOTNOTE_ID_PREFIX = 'fn'
FOOTNOTE_LINK_ID_PREFIX = 'fnref'
FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1
CSS_SECTION = "ol > li[id=#{FOOTNOTE_ID_PREFIX}#{FOOTNOTE_START_NUMBER}]"
FOOTNOTE_ID_PREFIX = 'fn-'
FOOTNOTE_LINK_ID_PREFIX = 'fnref-'
FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}.+\z/.freeze
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}.+\z/.freeze
CSS_SECTION = "ol > li a[href^=\"\##{FOOTNOTE_LINK_ID_PREFIX}\"]"
XPATH_SECTION = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION).freeze
CSS_FOOTNOTE = 'sup > a[id]'
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
return doc unless first_footnote = doc.at_xpath(XPATH_SECTION)
xpath_section = Feature.enabled?(:use_cmark_renderer) ? XPATH_SECTION : XPATH_SECTION_OLD
return doc unless first_footnote = doc.at_xpath(xpath_section)
# Sanitization stripped off the section wrapper - add it back in
first_footnote.parent.wrap('<section class="footnotes">')
if Feature.enabled?(:use_cmark_renderer)
first_footnote.parent.parent.parent.wrap('<section class="footnotes" data-footnotes>')
else
first_footnote.parent.wrap('<section class="footnotes">')
end
rand_suffix = "-#{random_number}"
modified_footnotes = {}
doc.xpath(XPATH_FOOTNOTE).each do |link_node|
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath("li[id=#{fn_id(ref_num)}]")
if Feature.enabled?(:use_cmark_renderer)
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
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath("li[id=#{fn_id(ref_num)}]")
footnote_node = doc.at_xpath(node_xpath)
if INTEGER_PATTERN.match?(ref_num) && (footnote_node || modified_footnotes[ref_num])
if footnote_node || modified_footnotes[ref_num]
next if Feature.disabled?(:use_cmark_renderer) && !INTEGER_PATTERN.match?(ref_num)
link_node[:href] += rand_suffix
link_node[:id] += rand_suffix
# Sanitization stripped off class - add it back in
link_node.parent.append_class('footnote-ref')
link_node['data-footnote-ref'] = nil if Feature.enabled?(:use_cmark_renderer)
unless modified_footnotes[ref_num]
footnote_node[:id] += rand_suffix
......@@ -55,6 +78,7 @@ module Banzai
if backref_node
backref_node[:href] += rand_suffix
backref_node.append_class('footnote-backref')
backref_node['data-footnote-backref'] = nil if Feature.enabled?(:use_cmark_renderer)
end
modified_footnotes[ref_num] = true
......@@ -72,11 +96,13 @@ module Banzai
end
def fn_id(num)
"#{FOOTNOTE_ID_PREFIX}#{num}"
prefix = Feature.enabled?(:use_cmark_renderer) ? FOOTNOTE_ID_PREFIX : FOOTNOTE_ID_PREFIX_OLD
"#{prefix}#{num}"
end
def fnref_id(num)
"#{FOOTNOTE_LINK_ID_PREFIX}#{num}"
prefix = Feature.enabled?(:use_cmark_renderer) ? FOOTNOTE_LINK_ID_PREFIX : FOOTNOTE_LINK_ID_PREFIX_OLD
"#{prefix}#{num}"
end
end
end
......
......@@ -13,8 +13,7 @@ module Banzai
EXTENSIONS = [
:autolink, # provides support for automatically converting URLs to anchor tags.
:strikethrough, # provides support for strikethroughs.
:table, # provides support for tables.
:tagfilter # strips out several "unsafe" HTML tags from being used: https://github.github.com/gfm/#disallowed-raw-html-extension-
:table # provides support for tables.
].freeze
PARSE_OPTIONS = [
......@@ -23,36 +22,63 @@ module Banzai
:VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze
RENDER_OPTIONS_C = [
:GITHUB_PRE_LANG, # use GitHub-style <pre lang> for fenced code blocks.
:FOOTNOTES, # render footnotes.
:FULL_INFO_STRING, # include full info strings of code blocks in separate attribute.
:UNSAFE # allow raw/custom HTML and unsafe links.
].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 = [
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
].freeze
RENDER_OPTIONS_SOURCEPOS = RENDER_OPTIONS + [
:SOURCEPOS # enable embedding of source position information
:UNSAFE # allow raw/custom HTML and unsafe links.
].freeze
def initialize(context)
@context = context
@renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options)
@context = context
@renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) if Feature.disabled?(:use_cmark_renderer)
end
def render(text)
doc = CommonMarker.render_doc(text, PARSE_OPTIONS, EXTENSIONS)
if Feature.enabled?(:use_cmark_renderer)
CommonMarker.render_html(text, render_options, extensions)
else
doc = CommonMarker.render_doc(text, PARSE_OPTIONS, extensions)
@renderer.render(doc)
@renderer.render(doc)
end
end
private
def extensions
if Feature.enabled?(:use_cmark_renderer)
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
@context[:no_sourcepos] ? RENDER_OPTIONS : RENDER_OPTIONS_SOURCEPOS
@context[:no_sourcepos] ? render_options_no_sourcepos : render_options_sourcepos
end
def render_options_no_sourcepos
Feature.enabled?(:use_cmark_renderer) ? RENDER_OPTIONS_C : RENDER_OPTIONS_RUBY
end
def render_options_sourcepos
render_options_no_sourcepos + [
:SOURCEPOS # enable embedding of source position information
].freeze
end
end
end
......
......@@ -8,10 +8,8 @@ module Banzai
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
CSS_A = 'a'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
CSS_A = 'a'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
def call
return doc unless result[:escaped_literals]
......@@ -34,12 +32,22 @@ module Banzai
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end
doc.xpath(XPATH_CODE).each do |node|
doc.xpath(lang_tag).each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end
doc
end
private
def lang_tag
if Feature.enabled?(:use_cmark_renderer)
Gitlab::Utils::Nokogiri.css_to_xpath('pre')
else
Gitlab::Utils::Nokogiri.css_to_xpath('code')
end
end
end
end
end
......@@ -5,18 +5,15 @@ require "asciidoctor_plantuml/plantuml"
module Banzai
module Filter
# HTML that replaces all `code plantuml` tags with PlantUML img tags.
# HTML that replaces all `lang plantuml` tags with PlantUML img tags.
#
class PlantumlFilter < HTML::Pipeline::Filter
CSS = 'pre > code[lang="plantuml"]'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call
return doc unless settings.plantuml_enabled? && doc.at_xpath(XPATH)
return doc unless settings.plantuml_enabled? && doc.at_xpath(lang_tag)
plantuml_setup
doc.xpath(XPATH).each do |node|
doc.xpath(lang_tag).each do |node|
img_tag = Nokogiri::HTML::DocumentFragment.parse(
Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {}))
node.parent.replace(img_tag)
......@@ -27,6 +24,15 @@ module Banzai
private
def lang_tag
@lang_tag ||=
if Feature.enabled?(:use_cmark_renderer)
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
def settings
Gitlab::CurrentSettings.current_application_settings
end
......
......@@ -54,8 +54,13 @@ module Banzai
return unless node.name == 'a' || node.name == 'li'
return unless node.has_attribute?('id')
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
if Feature.enabled?(:use_cmark_renderer)
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
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')
end
......
......@@ -11,7 +11,7 @@ module Banzai
class SyntaxHighlightFilter < HTML::Pipeline::Filter
include OutputSafety
PARAMS_DELIMITER = ':'
LANG_PARAMS_DELIMITER = ':'
LANG_PARAMS_ATTR = 'data-lang-params'
CSS = 'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code'
......@@ -27,7 +27,7 @@ module Banzai
def highlight_node(node)
css_classes = +'code highlight js-syntax-highlight'
lang, lang_params = parse_lang_params(node.attr('lang'))
lang, lang_params = parse_lang_params(node)
sourcepos = node.parent.attr('data-sourcepos')
retried = false
......@@ -56,7 +56,7 @@ module Banzai
retry
end
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{sourcepos}\"" : ""
sourcepos_attr = sourcepos ? "data-sourcepos=\"#{sourcepos}\"" : ''
highlighted = %(<pre #{sourcepos_attr} class="#{css_classes}"
lang="#{language}"
......@@ -69,13 +69,36 @@ module Banzai
private
def parse_lang_params(language)
def parse_lang_params(node)
node = node.parent if Feature.enabled?(:use_cmark_renderer)
# 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
# line, including language and its options. To keep backward compatability, we have to parse the old format and
# merge with the new one.
#
# Behaviors before separating language and its parameters:
# Old ones:
# "```ruby with options```" -> '<pre><code lang="ruby with options">'.
# "```ruby:with:options```" -> '<pre><code lang="ruby:with:options">'.
#
# New ones:
# "```ruby with options```" -> '<pre><code lang="ruby" data-meta="with options">'.
# "```ruby:with:options```" -> '<pre><code lang="ruby:with:options">'.
language = node.attr('lang')
return unless language
lang, params = language.split(PARAMS_DELIMITER, 2)
formatted_params = %(#{LANG_PARAMS_ATTR}="#{escape_once(params)}") if params
language, language_params = language.split(LANG_PARAMS_DELIMITER, 2)
if Feature.enabled?(:use_cmark_renderer)
language_params = [node.attr('data-meta'), language_params].compact.join(' ')
end
formatted_language_params = format_language_params(language_params)
[lang, formatted_params]
[language, formatted_language_params]
end
# Separate method so it can be instrumented.
......@@ -95,6 +118,12 @@ module Banzai
def use_rouge?(language)
(%w(math suggestion) + ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES).exclude?(language)
end
def format_language_params(language_params)
return if language_params.blank?
%(#{LANG_PARAMS_ATTR}="#{escape_once(language_params)}")
end
end
end
end
# 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
......
......@@ -7,7 +7,11 @@ module Gitlab
register_for 'gitlab-html-pipeline'
def format(node, lang, opts)
%(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
if Feature.enabled?(:use_cmark_renderer)
%(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
else
%(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
end
end
end
end
......
......@@ -5,34 +5,42 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::FootnoteFilter do
include FilterSpecHelper
# first[^1] and second[^second]
# rubocop:disable Style/AsciiComments
# first[^1] and second[^second] and third[^_😄_]
# [^1]: one
# [^second]: two
# [^_😄_]: three
# rubocop:enable Style/AsciiComments
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>
<<~EOF.strip_heredoc
<p>first<sup><a href="#fn-1" id="fnref-1">1</a></sup> and second<sup><a href="#fn-second" id="fnref-second">2</a></sup> and third<sup><a href="#fn-_%F0%9F%98%84_" id="fnref-_%F0%9F%98%84_">3</a></sup></p>
<ol>
<li id="fn1">
<p>one <a href="#fnref1">↩</a></p>
<li id="fn-1">
<p>one <a href="#fnref-1" aria-label="Back to content">↩</a></p>
</li>
<li id="fn2">
<p>two <a href="#fnref2">↩</a></p>
<li id="fn-second">
<p>two <a href="#fnref-second" aria-label="Back to content">↩</a></p>
</li>\n<li id="fn-_%F0%9F%98%84_">
<p>three <a href="#fnref-_%F0%9F%98%84_" aria-label="Back to content">↩</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>
<<~EOF.strip_heredoc
<p>first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-second-#{identifier}" id="fnref-second-#{identifier}" data-footnote-ref="">2</a></sup> and third<sup class="footnote-ref"><a href="#fn-_%F0%9F%98%84_-#{identifier}" id="fnref-_%F0%9F%98%84_-#{identifier}" data-footnote-ref="">3</a></sup></p>
<section class=\"footnotes\" data-footnotes><ol>
<li id="fn-1-#{identifier}">
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
<li id="fn-second-#{identifier}">
<p>two <a href="#fnref-second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
<li id="fn2-#{identifier}">
<p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p>
<li id="fn-_%F0%9F%98%84_-#{identifier}">
<p>three <a href="#fnref-_%F0%9F%98%84_-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
</ol></section>
EOF
......@@ -41,10 +49,56 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
context 'when footnotes exist' do
let(:doc) { filter(footnote) }
let(:link_node) { doc.css('sup > a').first }
let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
let(:identifier) { link_node[:id].delete_prefix('fnref-1-') }
it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote
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
......@@ -5,90 +5,125 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper
describe 'markdown engine from context' do
it 'defaults to CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect(instance).to receive(:render).and_return('test')
shared_examples_for 'renders correct markdown' do
describe 'markdown engine from context' do
it 'defaults to CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect(instance).to receive(:render).and_return('test')
end
filter('test')
end
filter('test')
end
it 'uses CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect(instance).to receive(:render).and_return('test')
end
it 'uses CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
expect(instance).to receive(:render).and_return('test')
filter('test', { markdown_engine: :common_mark })
end
filter('test', { markdown_engine: :common_mark })
end
end
describe 'code block' do
context 'using CommonMark' do
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
describe 'code block' do
context 'using CommonMark' do
before do
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)
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)
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)
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
it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```", no_sourcepos: true)
expect(result).to start_with('<pre><code lang="html">')
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
describe 'source line position' do
context 'using CommonMark' do
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
it 'works with utf8 chars in language' do
result = filter("```日\nsome code\n```", no_sourcepos: true)
it 'defaults to add data-sourcepos' do
result = filter('test')
expect(result).to start_with('<pre><code lang="日">')
end
expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
end
it 'works with additional language parameters' do
result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true)
it 'disables data-sourcepos' do
result = filter('test', no_sourcepos: true)
expect(result).to start_with('<pre><code lang="ruby:red gem">')
expect(result).to eq '<p>test</p>'
end
end
end
end
describe 'source line position' do
context 'using CommonMark' do
before do
stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
describe 'footnotes in tables' do
it 'processes footnotes in table cells' do
text = <<-MD.strip_heredoc
| Column1 |
| --------- |
| foot [^1] |
it 'defaults to add data-sourcepos' do
result = filter('test')
[^1]: a footnote
MD
expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
end
result = filter(text, no_sourcepos: true)
it 'disables data-sourcepos' do
result = filter('test', no_sourcepos: true)
expect(result).to include('<td>foot <sup')
expect(result).to eq '<p>test</p>'
if Feature.enabled?(:use_cmark_renderer)
expect(result).to include('<section class="footnotes" data-footnotes>')
else
expect(result).to include('<section class="footnotes">')
end
end
end
end
describe 'footnotes in tables' do
it 'processes footnotes in table cells' do
text = <<-MD.strip_heredoc
| Column1 |
| --------- |
| foot [^1] |
[^1]: a footnote
MD
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
result = filter(text, no_sourcepos: true)
it_behaves_like 'renders correct markdown'
end
expect(result).to include('<td>foot <sup')
expect(result).to include('<section class="footnotes">')
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end
it_behaves_like 'renders correct markdown'
end
end
......@@ -5,30 +5,67 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper
it 'replaces plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
doc = filter(input)
shared_examples_for 'renders correct markdown' do
it 'replaces plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
expect(doc.to_s).to eq output
input = if Feature.enabled?(:use_cmark_renderer)
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
else
'<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>'
doc = filter(input)
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)
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)
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
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>'
doc = filter(input)
expect(doc.to_s).to eq output
end
end
it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
doc = filter(input)
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
expect(doc.to_s).to eq output
it_behaves_like 'renders correct markdown'
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 = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
doc = filter(input)
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end
expect(doc.to_s).to eq output
it_behaves_like 'renders correct markdown'
end
end
......@@ -45,10 +45,10 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'allows `text-align` property in `style` attribute on table elements' do
html = <<~HTML
<table>
<tr><th style="text-align: center">Head</th></tr>
<tr><td style="text-align: right">Body</th></tr>
</table>
<table>
<tr><th style="text-align: center">Head</th></tr>
<tr><td style="text-align: right">Body</th></tr>
</table>
HTML
doc = filter(html)
......@@ -140,14 +140,14 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
describe 'footnotes' do
it 'allows correct footnote id property on links' do
exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
exp = %q(<a href="#fn-first" id="fnref-first">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>)
exp = %q(<ol><li id="fn-last">footnote</li></ol>)
act = filter(exp)
expect(act.to_html).to eq exp
......@@ -156,7 +156,7 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'removes invalid id for footnote links' do
exp = %q(<a href="#fn1">link</a>)
%w[fnrefx test xfnref1].each do |id|
%w[fnrefx test xfnref-1].each do |id|
act = filter(%(<a href="#fn1" id="#{id}">link</a>))
expect(act.to_html).to eq exp
......@@ -166,18 +166,58 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'removes invalid id for footnote li' do
exp = %q(<ol><li>footnote</li></ol>)
%w[fnx test xfn1].each do |id|
%w[fnx test xfn-1].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)
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
expect(act.to_html).to eq exp
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
......
......@@ -11,130 +11,210 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
# after Markdown rendering.
result = filter(%{<pre lang="#{lang}"><code>&lt;script&gt;alert(1)&lt;/script&gt;</code></pre>})
expect(result.to_html).not_to include("<script>alert(1)</script>")
expect(result.to_html).to include("alert(1)")
# `(1)` symbols are wrapped by lexer tags.
expect(result.to_html).not_to match(%r{<script>alert.*<\/script>})
# `<>` stands for lexer tags like <span ...>, not &lt;s above.
expect(result.to_html).to match(%r{alert(<.*>)?\((<.*>)?1(<.*>)?\)})
end
end
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
shared_examples_for 'renders correct markdown' do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
expect(result.to_html).to eq('<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>')
end
expect(result.to_html).to eq('<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>')
end
include_examples "XSS prevention", ""
end
include_examples "XSS prevention", ""
end
context "when contains mermaid diagrams" do
it "ignores mermaid blocks" do
result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
context "when contains mermaid diagrams" do
it "ignores mermaid blocks" do
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>')
expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
end
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code lang="ruby">def fun end</code></pre>')
context "when a valid language is specified" do
it "highlights as that language" do
result = if Feature.enabled?(:use_cmark_renderer)
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).to eq('<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>')
end
expect(result.to_html).to eq('<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>')
include_examples "XSS prevention", "ruby"
end
include_examples "XSS prevention", "ruby"
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = if Feature.enabled?(:use_cmark_renderer)
filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
else
filter('<pre><code lang="gnuplot">This is a test</code></pre>')
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code lang="gnuplot">This is a test</code></pre>')
expect(result.to_html).to eq('<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>')
end
expect(result.to_html).to eq('<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>')
include_examples "XSS prevention", "gnuplot"
end
include_examples "XSS prevention", "gnuplot"
end
context "languages that should be passed through" do
let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
let(:data_attr) { described_class::LANG_PARAMS_ATTR }
context "languages that should be passed through" do
let(:delimiter) { described_class::PARAMS_DELIMITER }
let(:data_attr) { described_class::LANG_PARAMS_ATTR }
%w(math mermaid plantuml suggestion).each do |lang|
context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do
result = if Feature.enabled?(:use_cmark_renderer)
filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
else
filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
end
%w(math mermaid plantuml suggestion).each do |lang|
context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
expect(result.to_html).to eq(%{<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>})
end
expect(result.to_html).to eq(%{<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>})
include_examples "XSS prevention", lang
end
include_examples "XSS prevention", lang
context "when #{lang} has extra params" do
let(:lang_params) { 'foo-bar-kux' }
let(:xss_lang) do
if Feature.enabled?(:use_cmark_renderer)
"#{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)
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).to eq(%{<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>})
end
include_examples "XSS prevention", lang
if Feature.enabled?(:use_cmark_renderer)
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
context "when #{lang} has extra params" do
let(:lang_params) { 'foo-bar-kux' }
context 'when multiple param delimiters are used' do
let(:lang) { 'suggestion' }
let(:lang_params) { '-1+10' }
it "includes data-lang-params tag with extra information" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
let(:expected_result) do
%{<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>}
end
expect(result.to_html).to eq(%{<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>})
context 'when delimiter is space' do
it 'delimits on the first appearance' do
if Feature.enabled?(:use_cmark_renderer)
result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
expect(result.to_html).to eq(expected_result)
else
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
expect(result.to_html).to eq(%{<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>})
end
end
end
include_examples "XSS prevention", lang
include_examples "XSS prevention",
"#{lang}#{described_class::PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
include_examples "XSS prevention",
"#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>"
context 'when delimiter is colon' do
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)
expect(result.to_html).to eq(expected_result)
else
expect(result.to_html).to eq(%{<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>})
end
end
end
end
end
context 'when multiple param delimiters are used' do
let(:lang) { 'suggestion' }
let(:lang_params) { '-1+10' }
context "when sourcepos metadata is available" do
it "includes it in the highlighted code block" do
result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
it "delimits on the first appearance" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
expect(result.to_html).to eq(%{<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>})
expect(result.to_html).to eq('<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>')
end
end
end
context "when sourcepos metadata is available" do
it "includes it in the highlighted code block" do
result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">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
expect(result.to_html).to eq('<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>')
end
end
it "highlights as plaintext" do
result = if Feature.enabled?(:use_cmark_renderer)
filter('<pre lang="ruby"><code>This is a test</code></pre>')
else
filter('<pre><code lang="ruby">This is a test</code></pre>')
end
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)
expect(result.to_html).to eq('<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>')
end
include_examples "XSS prevention", "ruby"
end
it "highlights as plaintext" do
result = filter('<pre><code lang="ruby">This is a test</code></pre>')
context "when Rouge lexing fails after a retry" do
before do
allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end
end
expect(result.to_html).to eq('<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>')
end
it "does not add highlighting classes" do
result = filter('<pre><code>This is a test</code></pre>')
expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
end
include_examples "XSS prevention", "ruby"
include_examples "XSS prevention", "ruby"
end
end
context "when Rouge lexing fails after a retry" do
context 'using ruby-based HTML renderer' do
before do
allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end
stub_feature_flags(use_cmark_renderer: false)
end
it "does not add highlighting classes" do
result = filter('<pre><code>This is a test</code></pre>')
it_behaves_like 'renders correct markdown'
end
expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end
include_examples "XSS prevention", "ruby"
it_behaves_like 'renders correct markdown'
end
end
......@@ -31,29 +31,29 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
describe 'footnotes' do
let(:project) { create(:project, :public) }
let(:html) { described_class.to_html(footnote_markdown, project: project) }
let(:identifier) { html[/.*fnref1-(\d+).*/, 1] }
let(:identifier) { html[/.*fnref-1-(\d+).*/, 1] }
let(:footnote_markdown) do
<<~EOF
first[^1] and second[^second] and twenty[^twenty]
first[^1] and second[^😄second] and twenty[^_twenty]
[^1]: one
[^second]: two
[^twenty]: twenty
[^😄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>
<<~EOF.strip_heredoc
<p dir="auto">first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-%F0%9F%98%84second-#{identifier}" id="fnref-%F0%9F%98%84second-#{identifier}" data-footnote-ref="">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn-_twenty-#{identifier}" id="fnref-_twenty-#{identifier}" data-footnote-ref="">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>
<section class="footnotes" data-footnotes><ol>
<li id="fn-1-#{identifier}">
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-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 id="fn-%F0%9F%98%84second-#{identifier}">
<p>two <a href="#fnref-%F0%9F%98%84second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-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 id="fn-_twenty-#{identifier}">
<p>twenty <a href="#fnref-_twenty-#{identifier}" aria-label="Back to content" class="footnote-backref" data-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
......@@ -64,6 +64,47 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
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
describe 'links are detected as malicious' do
......
......@@ -5,18 +5,7 @@ require 'spec_helper'
RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
using RSpec::Parameterized::TableSyntax
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)
result
end
shared_examples_for 'renders correct markdown' do
describe 'CommonMark tests', :aggregate_failures do
it 'converts all reference punctuation to literals' do
reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
......@@ -79,10 +68,19 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
end
describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
it 'renders correct html' do
if Feature.enabled?(:use_cmark_renderer)
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>)
%Q(``` foo\\@bar\nfoo\n```) | %Q(<code lang="foo@bar">foo\n</code>)
end
with_them do
......@@ -91,4 +89,33 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
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)
result
end
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
it_behaves_like 'renders correct markdown'
end
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end
it_behaves_like 'renders correct markdown'
end
end
end
......@@ -11,27 +11,13 @@ module Gitlab
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end
context "without project" do
let(:input) { '<b>ascii</b>' }
let(:context) { {} }
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
shared_examples_for 'renders correct asciidoc' do
context "without project" do
let(:input) { '<b>ascii</b>' }
let(:context) { {} }
let(:html) { 'H<sub>2</sub>O' }
context "with asciidoc_opts" do
it "merges the options with default ones" do
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
......@@ -42,796 +28,839 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
render(input, context)
expect(render(input, context)).to eq(html)
end
end
context "with requested path" do
input = <<~ADOC
Document name: {docname}.
ADOC
it "ignores {docname} when not available" do
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
context "with asciidoc_opts" do
it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
extensions: be_a(Proc)
}
context "XSS" do
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>"
},
'fenced code with inline script' => {
input: '```mypre"><script>alert(3)</script>',
output: "<div>\n<div>\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</div>\n</div>"
}
}
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
items.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
expect(render(data[:input], context)).to include(data[:output])
render(input, context)
end
end
it 'does not allow locked attributes to be overridden' do
context "with requested path" do
input = <<~ADOC
{counter:max-include-depth:1234}
<|-- {max-include-depth}
Document name: {docname}.
ADOC
expect(render(input, {})).not_to include('1234')
end
end
it "ignores {docname} when not available" do
expect(render(input, {})).to include(input.strip)
end
context "images" do
it "does lazy load and link image" do
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>"
expect(render(input, context)).to include(output)
[
['/', '', '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
it "does not automatically link image if link is explicitly defined" do
input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
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>"
expect(render(input, context)).to include(output)
end
end
context "XSS" do
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>"
}
}
context 'with admonition' do
it 'preserves classes' do
input = <<~ADOC
NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
ADOC
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
output = <<~HTML
<div class="admonitionblock">
<table>
<tr>
<td class="icon">
<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
# `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 =
if Feature.enabled?(:use_cmark_renderer)
"<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n</div>\n</div>"
else
"<div>\n<div>\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</div>\n</div>"
end
context 'with passthrough' do
it 'removes non heading ids' do
input = <<~ADOC
++++
<h2 id="foo">Title</h2>
++++
ADOC
expect(render(input, context)).to include(output)
end
output = <<~HTML
<h2>Title</h2>
HTML
it 'does not allow locked attributes to be overridden' do
input = <<~ADOC
{counter:max-include-depth:1234}
<|-- {max-include-depth}
ADOC
expect(render(input, context)).to include(output.strip)
expect(render(input, {})).not_to include('1234')
end
end
it 'removes non footnote def ids' do
input = <<~ADOC
++++
<div id="def">Footnote definition</div>
++++
ADOC
output = <<~HTML
<div>Footnote definition</div>
HTML
context "images" do
it "does lazy load and link image" do
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>"
expect(render(input, context)).to include(output)
end
expect(render(input, context)).to include(output.strip)
it "does not automatically link image if link is explicitly defined" do
input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
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>"
expect(render(input, context)).to include(output)
end
end
it 'removes non footnote ref ids' do
input = <<~ADOC
++++
<a id="ref">Footnote reference</a>
++++
ADOC
output = <<~HTML
<a>Footnote reference</a>
HTML
context 'with admonition' do
it 'preserves classes' do
input = <<~ADOC
NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
ADOC
expect(render(input, context)).to include(output.strip)
output = <<~HTML
<div class="admonitionblock">
<table>
<tr>
<td class="icon">
<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
context 'with footnotes' do
it 'preserves ids and links' do
input = <<~ADOC
This paragraph has a footnote.footnote:[This is the text of the footnote.]
ADOC
output = <<~HTML
<div>
<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
context 'with passthrough' do
it 'removes non heading ids' do
input = <<~ADOC
++++
<h2 id="foo">Title</h2>
++++
ADOC
context 'with section anchors' do
it 'preserves ids and links' do
input = <<~ADOC
= Title
output = <<~HTML
<h2>Title</h2>
HTML
== First section
expect(render(input, context)).to include(output.strip)
end
This is the first section.
it 'removes non footnote def ids' do
input = <<~ADOC
++++
<div id="def">Footnote definition</div>
++++
ADOC
== Second section
output = <<~HTML
<div>Footnote definition</div>
HTML
This is the second section.
expect(render(input, context)).to include(output.strip)
end
== Thunder ⚡ !
it 'removes non footnote ref ids' do
input = <<~ADOC
++++
<a id="ref">Footnote reference</a>
++++
ADOC
This is the third section.
ADOC
output = <<~HTML
<a>Footnote reference</a>
HTML
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)
expect(render(input, context)).to include(output.strip)
end
end
end
context 'with xrefs' do
it 'preserves ids' do
input = <<~ADOC
Learn how to xref:cross-references[use cross references].
[[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
<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
context 'with footnotes' do
it 'preserves ids and links' do
input = <<~ADOC
This paragraph has a footnote.footnote:[This is the text of the footnote.]
ADOC
expect(render(input, context)).to include(output.strip)
output = <<~HTML
<div>
<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
end
context 'with checklist' do
it 'preserves classes' do
input = <<~ADOC
* [x] checked
* [ ] not checked
ADOC
context 'with section anchors' do
it 'preserves ids and links' do
input = <<~ADOC
= Title
== First section
This is the first section.
== Second section
This is the second section.
== Thunder ⚡ !
This is the third section.
ADOC
output = <<~HTML
<div>
<ul class="checklist">
<li>
<p><i class="fa fa-check-square-o"></i> checked</p>
</li>
<li>
<p><i class="fa fa-square-o"></i> not checked</p>
</li>
</ul>
</div>
HTML
expect(render(input, context)).to include(output.strip)
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 marks' do
it 'preserves classes' do
input = <<~ADOC
Werewolves are allergic to #cassia cinnamon#.
Did the werewolves read the [.small]#small print#?
Where did all the [.underline.small]#cores# run off to?
context 'with xrefs' do
it 'preserves ids' do
input = <<~ADOC
Learn how to xref:cross-references[use cross references].
[[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
We need [.line-through]#ten# make that twenty VMs.
output = <<~HTML
<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
[.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)
expect(render(input, context)).to include(output.strip)
end
end
end
context 'with fenced block' do
it 'highlights syntax' do
input = <<~ADOC
```js
console.log('hello world')
```
ADOC
output = <<~HTML
<div>
<div>
<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>
</div>
</div>
HTML
context 'with checklist' do
it 'preserves classes' do
input = <<~ADOC
* [x] checked
* [ ] not checked
ADOC
expect(render(input, context)).to include(output.strip)
output = <<~HTML
<div>
<ul class="checklist">
<li>
<p><i class="fa fa-check-square-o"></i> checked</p>
</li>
<li>
<p><i class="fa fa-square-o"></i> not checked</p>
</li>
</ul>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
end
context 'with listing block' do
it 'highlights syntax' do
input = <<~ADOC
[source,c++]
.class.cpp
----
#include <stdio.h>
for (int i = 0; i < 5; i++) {
std::cout<<"*"<<std::endl;
}
----
ADOC
context 'with marks' do
it 'preserves classes' do
input = <<~ADOC
Werewolves are allergic to #cassia cinnamon#.
Did the werewolves read the [.small]#small print#?
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
<div>
<div>class.cpp</div>
<div>
<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>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
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 stem block' do
it 'does not apply syntax highlighting' do
input = <<~ADOC
[stem]
++++
\sqrt{4} = 2
++++
ADOC
context 'with fenced block' do
it 'highlights syntax' do
input = <<~ADOC
```js
console.log('hello world')
```
ADOC
output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
output = <<~HTML
<div>
<div>
<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>
</div>
</div>
HTML
expect(render(input, context)).to include(output)
expect(render(input, context)).to include(output.strip)
end
end
end
context 'external links' do
it 'adds the `rel` attribute to the link' do
output = render('link:https://google.com[Google]', context)
context 'with listing block' do
it 'highlights syntax' do
input = <<~ADOC
[source,c++]
.class.cpp
----
#include <stdio.h>
for (int i = 0; i < 5; i++) {
std::cout<<"*"<<std::endl;
}
----
ADOC
expect(output).to include('rel="nofollow noreferrer noopener"')
output = <<~HTML
<div>
<div>class.cpp</div>
<div>
<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>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
end
context 'LaTex code' do
it 'adds class js-render-math to the output' do
input = <<~MD
:stem: latexmath
[stem]
++++
\sqrt{4} = 2
++++
another part
[latexmath]
++++
\beta_x \gamma
++++
context 'with stem block' do
it 'does not apply syntax highlighting' do
input = <<~ADOC
[stem]
++++
\sqrt{4} = 2
++++
ADOC
stem:[2+2] is 4
MD
output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
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)
end
end
end
context 'outfilesuffix' do
it 'defaults to adoc' do
output = render("Inter-document reference <<README.adoc#>>", context)
context 'external links' do
it 'adds the `rel` attribute to the link' do
output = render('link:https://google.com[Google]', context)
expect(output).to include("a href=\"README.adoc\"")
expect(output).to include('rel="nofollow noreferrer noopener"')
end
end
end
context 'with mermaid diagrams' do
it 'adds class js-render-mermaid to the output' do
input = <<~MD
[mermaid]
....
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)
context 'LaTex code' do
it 'adds class js-render-math to the output' do
input = <<~MD
:stem: latexmath
[stem]
++++
\sqrt{4} = 2
++++
another part
[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
it 'applies subs in diagram block' do
input = <<~MD
:class-name: AveryLongClass
context 'outfilesuffix' do
it 'defaults to adoc' do
output = render("Inter-document reference <<README.adoc#>>", context)
[mermaid,subs=+attributes]
....
classDiagram
Class01 <|-- {class-name} : Cool
....
MD
expect(output).to include("a href=\"README.adoc\"")
end
end
output = <<~HTML
<pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
Class01 &lt;|-- AveryLongClass : Cool</pre>
HTML
context 'with mermaid diagrams' do
it 'adds class js-render-mermaid to the output' do
input = <<~MD
[mermaid]
....
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
expect(render(input, context)).to include(output.strip)
it 'applies subs in diagram block' do
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
context 'with Kroki enabled' do
before do
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')
end
context 'with Kroki enabled' do
before do
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')
end
it 'converts a graphviz diagram to image' do
input = <<~ADOC
[graphviz]
....
digraph G {
Hello->World
}
....
ADOC
it 'converts a graphviz diagram to image' do
input = <<~ADOC
[graphviz]
....
digraph G {
Hello->World
}
....
ADOC
output = <<~HTML
<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
output = <<~HTML
<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
expect(render(input, context)).to include(output.strip)
end
expect(render(input, context)).to include(output.strip)
end
it 'does not convert 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
it 'does not convert 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
<div>
<div>
<pre>blockdiag {
Kroki -&gt; generates -&gt; "Block diagrams";
Kroki -&gt; is -&gt; "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}</pre>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
output = <<~HTML
<div>
<div>
<pre>blockdiag {
Kroki -&gt; generates -&gt; "Block diagrams";
Kroki -&gt; is -&gt; "very easy!";
Kroki [color = "greenyellow"];
"Block diagrams" [color = "pink"];
"very easy!" [color = "orange"];
}</pre>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
it 'does not allow kroki-plantuml-include to be overridden' do
input = <<~ADOC
[plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
....
class BlockProcessor
it 'does not allow kroki-plantuml-include to be overridden' do
input = <<~ADOC
[plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
....
class BlockProcessor
BlockProcessor <|-- {counter:kroki-plantuml-include}
....
ADOC
BlockProcessor <|-- {counter:kroki-plantuml-include}
....
ADOC
output = <<~HTML
<div>
<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>
</div>
</div>
HTML
expect(render(input, {})).to include(output.strip)
end
output = <<~HTML
<div>
<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>
</div>
</div>
HTML
it 'does not allow kroki-server-url to be overridden' do
input = <<~ADOC
[plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
....
class BlockProcessor
BlockProcessor
....
ADOC
expect(render(input, {})).to include(output.strip)
expect(render(input, {})).not_to include('evilsite')
end
end
it 'does not allow kroki-server-url to be overridden' do
input = <<~ADOC
[plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
....
class BlockProcessor
context 'with Kroki and BlockDiag (additional format) enabled' do
before do
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_formats_blockdiag).and_return(true)
end
BlockProcessor
....
ADOC
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
expect(render(input, {})).not_to include('evilsite')
output = <<~HTML
<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
expect(render(input, context)).to include(output.strip)
end
end
end
context 'with Kroki and BlockDiag (additional format) enabled' do
before do
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_formats_blockdiag).and_return(true)
context 'with project' do
let(:context) do
{
commit: commit,
project: project,
ref: ref,
requested_path: requested_path
}
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
<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(:commit) { project.commit(ref) }
let(:project) { create(:project, :repository) }
let(:ref) { 'asciidoc' }
let(:requested_path) { '/' }
expect(render(input, context)).to include(output.strip)
end
end
end
context 'include directive' do
subject(:output) { render(input, context) }
context 'with project' do
let(:context) do
{
commit: commit,
project: project,
ref: ref,
requested_path: requested_path
}
end
let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
let(:commit) { project.commit(ref) }
let(:project) { create(:project, :repository) }
let(:ref) { 'asciidoc' }
let(:requested_path) { '/' }
context 'include directive' do
subject(:output) { render(input, context) }
before do
current_file = requested_path
current_file += 'README.adoc' if requested_path.end_with? '/'
let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
create_file(current_file, "= AsciiDoc\n")
end
before do
current_file = requested_path
current_file += 'README.adoc' if requested_path.end_with? '/'
def many_includes(target)
Array.new(10, "include::#{target}[]").join("\n")
end
create_file(current_file, "= AsciiDoc\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
def many_includes(target)
Array.new(10, "include::#{target}[]").join("\n")
end
let(:include_path) { 'a.adoc' }
let(:requested_path) { 'doc/api/README.md' }
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'))
it 'completes successfully' do
is_expected.to include('<p>Include this:</p>')
end
end
let(:include_path) { 'a.adoc' }
let(:requested_path) { 'doc/api/README.md' }
context 'with path to non-existing file' do
let(:include_path) { 'not-exists.adoc' }
it 'completes successfully' do
is_expected.to include('<p>Include this:</p>')
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 non-existing file' do
let(:include_path) { 'not-exists.adoc' }
shared_examples :invalid_include do
let(:include_path) { 'dk.png' }
it 'renders Unresolved directive placeholder' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end
end
before do
allow(project.repository).to receive(:blob_at).and_return(blob)
end
shared_examples :invalid_include do
let(:include_path) { 'dk.png' }
it 'does not read the blob' do
expect(blob).not_to receive(:data)
end
before do
allow(project.repository).to receive(:blob_at).and_return(blob)
it 'renders Unresolved directive placeholder' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end
end
it 'does not read the blob' do
expect(blob).not_to receive(:data)
end
context 'with path to a binary file' do
let(:blob) { fake_blob(path: 'dk.png', binary: true) }
it 'renders Unresolved directive placeholder' do
is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
include_examples :invalid_include
end
end
context 'with path to a binary file' do
let(:blob) { fake_blob(path: 'dk.png', binary: true) }
include_examples :invalid_include
end
context 'with path to file in external storage' do
let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
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
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
project.update_attribute(:lfs_enabled, true)
include_examples :invalid_include
end
include_examples :invalid_include
end
context 'with path to a textual file' do
let(:include_path) { 'sample.adoc' }
context 'with path to a textual file' do
let(:include_path) { 'sample.adoc' }
before do
create_file(file_path, "Content from #{include_path}")
end
before do
create_file(file_path, "Content from #{include_path}")
end
shared_examples :valid_include do
[
['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
['../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']
].each do |include_path_, file_path_, desc|
context "the file is specified by #{desc}" do
let(:include_path) { include_path_ }
let(:file_path) { file_path_ }
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>")
shared_examples :valid_include do
[
['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
['../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']
].each do |include_path_, file_path_, desc|
context "the file is specified by #{desc}" do
let(:include_path) { include_path_ }
let(:file_path) { file_path_ }
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
context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.adoc' }
context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.adoc' }
include_examples :valid_include
include_examples :valid_include
context 'without a commit (only ref)' do
let(:commit) { nil }
context 'without a commit (only ref)' do
let(:commit) { nil }
include_examples :valid_include
include_examples :valid_include
end
end
end
context 'when requested path is a directory in the repo' do
let(:requested_path) { 'doc/api/' }
context 'when requested path is a directory in the repo' do
let(:requested_path) { 'doc/api/' }
include_examples :valid_include
include_examples :valid_include
context 'without a commit (only ref)' do
let(:commit) { nil }
context 'without a commit (only ref)' do
let(:commit) { nil }
include_examples :valid_include
include_examples :valid_include
end
end
end
end
context 'when repository is passed into the context' do
let(:wiki_repo) { project.wiki.repository }
let(:include_path) { 'wiki_file.adoc' }
before do
project.create_wiki
context.merge!(repository: wiki_repo)
end
context 'when repository is passed into the context' do
let(:wiki_repo) { project.wiki.repository }
let(:include_path) { 'wiki_file.adoc' }
context 'when the file exists' do
before do
create_file(include_path, 'Content from wiki', repository: wiki_repo)
project.create_wiki
context.merge!(repository: wiki_repo)
end
it { is_expected.to include('<p>Content from wiki</p>') }
end
context 'when the file does not exist' do
it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
end
end
context 'recursive includes with relative paths' do
let(:input) do
<<~ADOC
Source: requested file
context 'when the file exists' do
before do
create_file(include_path, 'Content from wiki', repository: wiki_repo)
end
include::doc/README.adoc[]
it { is_expected.to include('<p>Content from wiki</p>') }
end
include::license.adoc[]
ADOC
context 'when the file does not exist' do
it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
end
end
before do
create_file 'doc/README.adoc', <<~ADOC
Source: doc/README.adoc
include::../license.adoc[]
context 'recursive includes with relative paths' do
let(:input) do
<<~ADOC
Source: requested file
include::doc/README.adoc[]
include::license.adoc[]
ADOC
end
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
before do
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
include::./common.adoc[]
ADOC
create_file 'doc/api/common.adoc', <<~ADOC
Source: doc/api/common.adoc
ADOC
it 'includes content of the included files recursively' do
expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
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
it 'includes content of the included files recursively' do
expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
Source: requested file
Source: doc/README.adoc
Source: license.adoc
Source: doc/api/hello.adoc
Source: doc/api/common.adoc
Source: license.adoc
ADOC
def create_file(path, content, repository: project.repository)
repository.create_file(project.creator, path, content,
message: "Add #{path}", branch_name: 'asciidoc')
end
end
end
end
def create_file(path, content, repository: project.repository)
repository.create_file(project.creator, path, content,
message: "Add #{path}", branch_name: 'asciidoc')
end
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
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
it_behaves_like 'renders correct asciidoc'
end
def render(*args)
......
......@@ -92,9 +92,16 @@ module StubGitlabCalls
end
def stub_commonmark_sourcepos_disabled
render_options =
if Feature.enabled?(:use_cmark_renderer)
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C
else
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY
end
allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
.to receive(:render_options)
.and_return(Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS)
.and_return(render_options)
end
private
......
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