diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 391ce83009eab69d8268adbbb0dcc1ad0b7376ea..31eb7a2b06c0f26545947662c68fb9255b4fcd86 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -740,6 +740,19 @@ position: relative; bottom: -3px; } + + /* Custom classes we explicitly support */ + .glfm-float-left { + float: left; + } + + .glfm-float-right { + float: right; + } + + .glfm-float-none { + float: none; + } } /** diff --git a/lib/banzai/filter/attributes_filter.rb b/lib/banzai/filter/attributes_filter.rb index 27fe865f63845f37eda5aba14883ee8d57be563c..3a15e3905758443dfd3a651b364743d967579c53 100644 --- a/lib/banzai/filter/attributes_filter.rb +++ b/lib/banzai/filter/attributes_filter.rb @@ -5,11 +5,12 @@ module Filter # Looks for attributes that are specified for an element. Follows the basic syntax laid out # in https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/attributes.md # For example, - # ![](http://example.com/image.jpg){width=50%} + # ![](http://example.com/image.jpg){width=50% .float-right} # # However we currently have the following limitations: # - only support images # - only support the `width` and `height` attributes + # - only support the `.float-right`, `.float-left`, and `.float-none` classes # - attributes can not span multiple lines # - unsupported attributes are thrown away class AttributesFilter < HTML::Pipeline::Filter @@ -18,10 +19,27 @@ class AttributesFilter < HTML::Pipeline::Filter CSS = 'img' XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + # Matches the attributes string between the curly braces i.e. { width=50% .float-right } ATTRIBUTES_PATTERN = %r{\A(?\{(?.{1,100})\})} - WIDTH_HEIGHT_REGEX = %r{\A(?height|width)="?(?[\w%]{1,10})"?\z} + + # Validator regex to check if the value is a valid size VALID_SIZE_REGEX = %r{\A\d{1,4}(%|px)?\z} + # Allowed values and their corresponding value validator regex + VALUE_REGEX = %r{\A(?[\w-]+)="?(?[^"\s]+)"?\z} + ALLOWED_VALUES = { + height: { value_regex: VALID_SIZE_REGEX }, + width: { value_regex: VALID_SIZE_REGEX } + }.freeze + + # Allowed classes and their corresponding class selector + CLASS_REGEX = %r{\A\.(?[\w-]+)\z} + ALLOWED_CLASSES = { + 'float-right': { class: 'glfm-float-right' }, + 'float-left': { class: 'glfm-float-left' }, + 'float-none': { class: 'glfm-float-none' } + }.freeze + def call doc.xpath(XPATH).each do |img| sibling = img.next @@ -30,12 +48,7 @@ def call match = sibling.content.match(ATTRIBUTES_PATTERN) next unless match && match[:attributes] - match[:attributes].split(' ').each do |attribute| - next unless attribute.match?(WIDTH_HEIGHT_REGEX) - - attribute_match = attribute.match(WIDTH_HEIGHT_REGEX) - img[attribute_match[:name].to_sym] = attribute_match[:size] if valid_size?(attribute_match[:size]) - end + match[:attributes].split(' ').each { |attribute| process_attribute(img, attribute) } sibling.content = sibling.content.sub(match[:matched], '') end @@ -45,8 +58,25 @@ def call private - def valid_size?(size) - size.match?(VALID_SIZE_REGEX) + def process_attribute(node, attribute) + case attribute + when VALUE_REGEX then handle_value_attribute(node, $~) + when CLASS_REGEX then handle_class_attribute(node, $~) + end + end + + def handle_value_attribute(node, match) + name = match[:name].to_sym + value = match[:value] + node[name] = value if ALLOWED_VALUES[name]&.dig(:value_regex)&.match?(value) + end + + def handle_class_attribute(node, match) + class_name = match[:class_name] + + return if (mapped_class_name = ALLOWED_CLASSES[class_name.to_sym]&.dig(:class)).blank? + + node[:class] = [node[:class], mapped_class_name].compact.join(' ') end end end diff --git a/spec/lib/banzai/filter/attributes_filter_spec.rb b/spec/lib/banzai/filter/attributes_filter_spec.rb index 7629d6bf17159fe9dcc5f19dba2be16c7993f96e..8e98f20380e89357a2be6fb8e6e356aa2309f00f 100644 --- a/spec/lib/banzai/filter/attributes_filter_spec.rb +++ b/spec/lib/banzai/filter/attributes_filter_spec.rb @@ -45,34 +45,66 @@ def image end end - describe 'height and width' do - context 'when size attributes are valid' do - where(:text, :result) do - "#{image}{width=100 height=200px}" | '' - "#{image}{width=100}" | '' - "#{image}{width=100px}" | '' - "#{image}{height=100%}" | '' - "#{image}{width=\"100%\"}" | '' + describe 'value attributes' do + describe 'height and width' do + context 'when size attributes are valid' do + where(:text, :result) do + "#{image}{width=100 height=200px}" | '' + "#{image}{width=100}" | '' + "#{image}{width=100px}" | '' + "#{image}{height=100%}" | '' + "#{image}{width=\"100%\"}" | '' + end + + with_them do + it 'adds them to the img' do + expect(filter(text).to_html).to eq result + end + end end - with_them do - it 'adds them to the img' do - expect(filter(text).to_html).to eq result + context 'when size attributes are invalid' do + where(:text, :result) do + "#{image}{width=100cs}" | '' + "#{image}{width=auto height=200}" | '' + "#{image}{width=10000}" | '' + "#{image}{width=-200}" | '' + end + + with_them do + it 'ignores them' do + expect(filter(text).to_html).to eq result + end end end end + end - context 'when size attributes are invalid' do - where(:text, :result) do - "#{image}{width=100cs}" | '' - "#{image}{width=auto height=200}" | '' - "#{image}{width=10000}" | '' - "#{image}{width=-200}" | '' + describe 'class attributes' do + describe 'float classes' do + context 'when float class is valid' do + where(:text, :result) do + "#{image}{.float-right}" | '' + "#{image}{.float-left}" | '' + "#{image}{.float-none}" | '' + end + + with_them do + it 'adds them to the img' do + expect(filter(text).to_html).to eq result + end + end end - with_them do - it 'ignores them' do - expect(filter(text).to_html).to eq result + context 'when float class is invalid' do + where(:text, :result) do + "#{image}{.float-center}" | '' + end + + with_them do + it 'ignores them' do + expect(filter(text).to_html).to eq result + end end end end