Commit ad534355 authored by blackst0ne's avatar blackst0ne Committed by Bob Van Landuyt

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

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