From f7d0439e95d5c55ec51c12edd867033ac1b34499 Mon Sep 17 00:00:00 2001 From: cmacleod Date: Fri, 13 Jun 2025 17:17:01 -0700 Subject: [PATCH 1/2] Add custom float classes to typography styles and enhance attributes filter for class support --- .../stylesheets/framework/typography.scss | 13 +++++ lib/banzai/filter/attributes_filter.rb | 50 +++++++++++++++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 391ce83009eab6..31eb7a2b06c0f2 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 27fe865f63845f..46d62ac217331b 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 -- GitLab From e54a1f74080d9c9f9d3def4e70b59c232a15481d Mon Sep 17 00:00:00 2001 From: cmacleod Date: Fri, 13 Jun 2025 17:56:22 -0700 Subject: [PATCH 2/2] Fix filter classes and add spec --- lib/banzai/filter/attributes_filter.rb | 6 +- .../banzai/filter/attributes_filter_spec.rb | 72 +++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/lib/banzai/filter/attributes_filter.rb b/lib/banzai/filter/attributes_filter.rb index 46d62ac217331b..3a15e390575844 100644 --- a/lib/banzai/filter/attributes_filter.rb +++ b/lib/banzai/filter/attributes_filter.rb @@ -35,9 +35,9 @@ class AttributesFilter < HTML::Pipeline::Filter # 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' } + 'float-right': { class: 'glfm-float-right' }, + 'float-left': { class: 'glfm-float-left' }, + 'float-none': { class: 'glfm-float-none' } }.freeze def call diff --git a/spec/lib/banzai/filter/attributes_filter_spec.rb b/spec/lib/banzai/filter/attributes_filter_spec.rb index 7629d6bf17159f..8e98f20380e893 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 -- GitLab