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

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

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

See merge request gitlab-org/gitlab!61792
parents 9a04cff7 ad534355
---
name: use_cmark_renderer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61792
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345744
milestone: '14.6'
type: development
group: group::project management
default_enabled: false
...@@ -16,37 +16,60 @@ module Banzai ...@@ -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[id=#{FOOTNOTE_ID_PREFIX}#{FOOTNOTE_START_NUMBER}]" CSS_SECTION = "ol > li a[href^=\"\##{FOOTNOTE_LINK_ID_PREFIX}\"]"
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
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">') 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|
if Feature.enabled?(:use_cmark_renderer)
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) 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)}]") 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
......
...@@ -10,8 +10,6 @@ module Banzai ...@@ -10,8 +10,6 @@ module Banzai
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')
if Feature.enabled?(:use_cmark_renderer)
return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN
return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
else
return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN_OLD
return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN_OLD
end
node.remove_attribute('id') node.remove_attribute('id')
end end
......
...@@ -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
[lang, formatted_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)
[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,9 +7,13 @@ module Gitlab ...@@ -7,9 +7,13 @@ module Gitlab
register_for 'gitlab-html-pipeline' register_for 'gitlab-html-pipeline'
def format(node, lang, opts) def format(node, lang, opts)
if Feature.enabled?(:use_cmark_renderer)
%(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
else
%(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>) %(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
end end
end end
end end
end end
end
end end
...@@ -5,6 +5,57 @@ require 'spec_helper' ...@@ -5,6 +5,57 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::FootnoteFilter do RSpec.describe Banzai::Filter::FootnoteFilter do
include FilterSpecHelper include FilterSpecHelper
# rubocop:disable Style/AsciiComments
# first[^1] and second[^second] and third[^_😄_]
# [^1]: one
# [^second]: two
# [^_😄_]: three
# rubocop:enable Style/AsciiComments
let(:footnote) do
<<~EOF.strip_heredoc
<p>first<sup><a href="#fn-1" id="fnref-1">1</a></sup> and second<sup><a href="#fn-second" id="fnref-second">2</a></sup> and third<sup><a href="#fn-_%F0%9F%98%84_" id="fnref-_%F0%9F%98%84_">3</a></sup></p>
<ol>
<li id="fn-1">
<p>one <a href="#fnref-1" aria-label="Back to content">↩</a></p>
</li>
<li id="fn-second">
<p>two <a href="#fnref-second" aria-label="Back to content">↩</a></p>
</li>\n<li id="fn-_%F0%9F%98%84_">
<p>three <a href="#fnref-_%F0%9F%98%84_" aria-label="Back to content">↩</a></p>
</li>
</ol>
EOF
end
let(:filtered_footnote) do
<<~EOF.strip_heredoc
<p>first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-second-#{identifier}" id="fnref-second-#{identifier}" data-footnote-ref="">2</a></sup> and third<sup class="footnote-ref"><a href="#fn-_%F0%9F%98%84_-#{identifier}" id="fnref-_%F0%9F%98%84_-#{identifier}" data-footnote-ref="">3</a></sup></p>
<section class=\"footnotes\" data-footnotes><ol>
<li id="fn-1-#{identifier}">
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
<li id="fn-second-#{identifier}">
<p>two <a href="#fnref-second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
<li id="fn-_%F0%9F%98%84_-#{identifier}">
<p>three <a href="#fnref-_%F0%9F%98%84_-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
</ol></section>
EOF
end
context 'when footnotes exist' do
let(:doc) { filter(footnote) }
let(:link_node) { doc.css('sup > a').first }
let(:identifier) { link_node[:id].delete_prefix('fnref-1-') }
it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote
end
context 'using ruby-based HTML renderer' do
# first[^1] and second[^second] # first[^1] and second[^second]
# [^1]: one # [^1]: one
# [^second]: two # [^second]: two
...@@ -38,13 +89,16 @@ RSpec.describe Banzai::Filter::FootnoteFilter do ...@@ -38,13 +89,16 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
EOF EOF
end end
context 'when footnotes exist' do
let(:doc) { filter(footnote) } let(:doc) { filter(footnote) }
let(:link_node) { doc.css('sup > a').first }
let(:identifier) { link_node[:id].delete_prefix('fnref1-') } let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
before do
stub_feature_flags(use_cmark_renderer: false)
end
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
end end
end
end end
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::MarkdownFilter do RSpec.describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper include FilterSpecHelper
shared_examples_for 'renders correct markdown' do
describe 'markdown engine from context' do describe 'markdown engine from context' do
it 'defaults to CommonMark' do it 'defaults to CommonMark' do
expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance| expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
...@@ -32,8 +33,12 @@ RSpec.describe Banzai::Filter::MarkdownFilter do ...@@ -32,8 +33,12 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
it 'adds language to lang attribute when specified' do it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```", no_sourcepos: true) 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">') expect(result).to start_with('<pre><code lang="html">')
end end
end
it 'does not add language to lang attribute when not specified' do it 'does not add language to lang attribute when not specified' do
result = filter("```\nsome code\n```", no_sourcepos: true) result = filter("```\nsome code\n```", no_sourcepos: true)
...@@ -44,13 +49,21 @@ RSpec.describe Banzai::Filter::MarkdownFilter do ...@@ -44,13 +49,21 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
it 'works with utf8 chars in language' do it 'works with utf8 chars in language' do
result = filter("```日\nsome code\n```", no_sourcepos: true) 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="日">') expect(result).to start_with('<pre><code lang="日">')
end end
end
it 'works with additional language parameters' do it 'works with additional language parameters' do
result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true) result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
expect(result).to start_with('<pre><code lang="ruby:red gem">') 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
end end
...@@ -88,7 +101,29 @@ RSpec.describe Banzai::Filter::MarkdownFilter do ...@@ -88,7 +101,29 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
result = filter(text, no_sourcepos: true) result = filter(text, no_sourcepos: true)
expect(result).to include('<td>foot <sup') expect(result).to include('<td>foot <sup')
if Feature.enabled?(:use_cmark_renderer)
expect(result).to include('<section class="footnotes" data-footnotes>')
else
expect(result).to include('<section class="footnotes">') expect(result).to include('<section class="footnotes">')
end end
end end
end
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
...@@ -5,9 +5,16 @@ require 'spec_helper' ...@@ -5,9 +5,16 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::PlantumlFilter do RSpec.describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper include FilterSpecHelper
shared_examples_for 'renders correct markdown' do
it 'replaces plantuml pre tag with img tag' do it 'replaces plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
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>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
doc = filter(input) doc = filter(input)
...@@ -16,8 +23,15 @@ RSpec.describe Banzai::Filter::PlantumlFilter do ...@@ -16,8 +23,15 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
it 'does not replace plantuml pre tag with img tag if disabled' do it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false) 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>' input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>' output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
end
doc = filter(input) doc = filter(input)
expect(doc.to_s).to eq output expect(doc.to_s).to eq output
...@@ -25,10 +39,33 @@ RSpec.describe Banzai::Filter::PlantumlFilter do ...@@ -25,10 +39,33 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
it 'does not replace plantuml pre tag with img tag if url is invalid' do it 'does not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
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>' 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) doc = filter(input)
expect(doc.to_s).to eq output expect(doc.to_s).to eq output
end end
end
context 'using ruby-based HTML renderer' do
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
...@@ -139,6 +139,45 @@ RSpec.describe Banzai::Filter::SanitizationFilter do ...@@ -139,6 +139,45 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
end end
describe 'footnotes' do describe 'footnotes' do
it 'allows correct footnote id property on links' do
exp = %q(<a href="#fn-first" id="fnref-first">foo/bar.md</a>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'allows correct footnote id property on li element' do
exp = %q(<ol><li id="fn-last">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 xfnref-1].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 xfn-1].each do |id|
act = filter(%(<ol><li id="#{id}">footnote</li></ol>))
expect(act.to_html).to eq exp
end
end
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
it 'allows correct footnote id property on links' do it 'allows correct footnote id property on links' do
exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>) exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
act = filter(exp) act = filter(exp)
...@@ -181,4 +220,5 @@ RSpec.describe Banzai::Filter::SanitizationFilter do ...@@ -181,4 +220,5 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
end end
end end
end end
end
end end
...@@ -11,11 +11,15 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -11,11 +11,15 @@ 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
shared_examples_for 'renders correct markdown' do
context "when no language is specified" do context "when no language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>') result = filter('<pre><code>def fun end</code></pre>')
...@@ -36,7 +40,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -36,7 +40,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
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>') 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 end
...@@ -46,7 +54,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -46,7 +54,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when an invalid language is specified" do context "when an invalid language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code lang="gnuplot">This is a test</code></pre>') 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
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>') 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
...@@ -55,13 +67,17 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -55,13 +67,17 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end end
context "languages that should be passed through" do context "languages that should be passed through" do
let(:delimiter) { described_class::PARAMS_DELIMITER } let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
let(:data_attr) { described_class::LANG_PARAMS_ATTR } let(:data_attr) { described_class::LANG_PARAMS_ATTR }
%w(math mermaid plantuml suggestion).each do |lang| %w(math mermaid plantuml suggestion).each do |lang|
context "when #{lang} is specified" do context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>}) 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
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>}) expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
end end
...@@ -72,17 +88,36 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -72,17 +88,36 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when #{lang} has extra params" do context "when #{lang} has extra params" do
let(:lang_params) { 'foo-bar-kux' } 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 it "includes data-lang-params tag with extra information" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>}) 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>}) 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 end
include_examples "XSS prevention", lang include_examples "XSS prevention", lang
if Feature.enabled?(:use_cmark_renderer)
include_examples "XSS prevention", include_examples "XSS prevention",
"#{lang}#{described_class::PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;" "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
include_examples "XSS prevention", include_examples "XSS prevention",
"#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>" "#{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 end
...@@ -90,7 +125,17 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -90,7 +125,17 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
let(:lang) { 'suggestion' } let(:lang) { 'suggestion' }
let(:lang_params) { '-1+10' } let(:lang_params) { '-1+10' }
it "delimits on the first appearance" do let(:expected_result) do
%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}
end
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>}) result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) expect(result.to_html).to eq(%{<pre 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>})
...@@ -98,6 +143,20 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -98,6 +143,20 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end end
end end
context 'when delimiter is colon' do
it 'delimits on the first appearance' do
result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
if Feature.enabled?(:use_cmark_renderer)
expect(result.to_html).to eq(expected_result)
else
expect(result.to_html).to eq(%{<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">This is a test</span></code></pre>})
end
end
end
end
end
context "when sourcepos metadata is available" do context "when sourcepos metadata is available" do
it "includes it in the highlighted code block" do it "includes it in the highlighted code block" do
result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>') result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
...@@ -114,7 +173,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -114,7 +173,11 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end end
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code lang="ruby">This is a test</code></pre>') result = if Feature.enabled?(:use_cmark_renderer)
filter('<pre lang="ruby"><code>This is a test</code></pre>')
else
filter('<pre><code lang="ruby">This is a test</code></pre>')
end
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>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre>')
end end
...@@ -137,4 +200,21 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do ...@@ -137,4 +200,21 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
include_examples "XSS prevention", "ruby" include_examples "XSS prevention", "ruby"
end end
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
...@@ -30,6 +30,42 @@ RSpec.describe Banzai::Pipeline::FullPipeline do ...@@ -30,6 +30,42 @@ 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(:identifier) { html[/.*fnref-1-(\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.strip_heredoc
<p dir="auto">first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-%F0%9F%98%84second-#{identifier}" id="fnref-%F0%9F%98%84second-#{identifier}" data-footnote-ref="">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn-_twenty-#{identifier}" id="fnref-_twenty-#{identifier}" data-footnote-ref="">3</a></sup></p>
<section class="footnotes" data-footnotes><ol>
<li id="fn-1-#{identifier}">
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
<li id="fn-%F0%9F%98%84second-#{identifier}">
<p>two <a href="#fnref-%F0%9F%98%84second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
<li id="fn-_twenty-#{identifier}">
<p>twenty <a href="#fnref-_twenty-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
</ol></section>
EOF
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
context 'using ruby-based HTML renderer' do
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[/.*fnref1-(\d+).*/, 1] }
let(:footnote_markdown) do let(:footnote_markdown) do
...@@ -59,12 +95,17 @@ RSpec.describe Banzai::Pipeline::FullPipeline do ...@@ -59,12 +95,17 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
EOF EOF
end end
before do
stub_feature_flags(use_cmark_renderer: false)
end
it 'properly adds the necessary ids and classes' do it 'properly adds the necessary ids and classes' do
stub_commonmark_sourcepos_disabled stub_commonmark_sourcepos_disabled
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
end end
end end
end
describe 'links are detected as malicious' do describe 'links are detected as malicious' do
it 'has tooltips for malicious links' do it 'has tooltips for malicious links' 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,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end end
shared_examples_for 'renders correct asciidoc' do
context "without project" do context "without project" do
let(:input) { '<b>ascii</b>' } let(:input) { '<b>ascii</b>' }
let(:context) { {} } let(:context) { {} }
...@@ -80,10 +81,6 @@ module Gitlab ...@@ -80,10 +81,6 @@ module Gitlab
'image with onerror' => { 'image with onerror' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', 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>" 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>"
} }
} }
...@@ -93,6 +90,21 @@ module Gitlab ...@@ -93,6 +90,21 @@ module Gitlab
end end
end end
# `stub_feature_flags method` runs AFTER declaration of `items` above.
# So the spec in its current implementation won't pass.
# Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
it "does not convert dangerous fenced code with inline script into HTML" do
input = '```mypre"><script>alert(3)</script>'
output =
if Feature.enabled?(:use_cmark_renderer)
"<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n</div>\n</div>"
else
"<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
end
expect(render(input, context)).to include(output)
end
it 'does not allow locked attributes to be overridden' do it 'does not allow locked attributes to be overridden' do
input = <<~ADOC input = <<~ADOC
{counter:max-include-depth:1234} {counter:max-include-depth:1234}
...@@ -833,6 +845,23 @@ module Gitlab ...@@ -833,6 +845,23 @@ module Gitlab
end end
end end
end end
end
context 'using ruby-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: false)
end
it_behaves_like 'renders correct asciidoc'
end
context 'using c-based HTML renderer' do
before do
stub_feature_flags(use_cmark_renderer: true)
end
it_behaves_like 'renders correct asciidoc'
end
def render(*args) def render(*args)
described_class.render(*args) described_class.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