primer_view_components 0.0.60 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -1
  3. data/app/components/primer/alpha/layout.html.erb +5 -0
  4. data/app/components/primer/alpha/layout.rb +276 -0
  5. data/app/components/primer/base_button.rb +1 -5
  6. data/app/components/primer/base_component.rb +7 -2
  7. data/app/components/primer/beta/blankslate.html.erb +15 -0
  8. data/app/components/primer/beta/blankslate.rb +240 -0
  9. data/app/components/primer/blankslate_component.rb +1 -1
  10. data/app/components/primer/component.rb +2 -2
  11. data/app/components/primer/hellip_button.rb +39 -0
  12. data/app/components/primer/hidden_text_expander.rb +18 -6
  13. data/app/components/primer/subhead_component.rb +1 -1
  14. data/lib/primer/classify.rb +3 -3
  15. data/lib/primer/view_components/engine.rb +1 -1
  16. data/lib/primer/view_components/linters/base_linter.rb +3 -52
  17. data/lib/primer/view_components/linters/blankslate_api_migration.rb +146 -0
  18. data/lib/primer/view_components/linters/blankslate_component_migration_counter.rb +1 -1
  19. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +2 -4
  20. data/lib/primer/view_components/linters/helpers/rubocop_helpers.rb +14 -0
  21. data/lib/primer/view_components/linters/tag_tree_helpers.rb +61 -0
  22. data/lib/primer/view_components/linters/two_column_layout_migration_counter.rb +158 -0
  23. data/lib/primer/view_components/version.rb +1 -1
  24. data/lib/tasks/docs.rake +50 -1
  25. data/static/arguments.yml +52 -67
  26. data/static/audited_at.json +5 -0
  27. data/static/classes.yml +20 -2
  28. data/static/constants.json +103 -0
  29. data/static/statuses.json +6 -1
  30. metadata +11 -2
@@ -6,7 +6,7 @@ module Primer
6
6
  # `Blankslate` renders an `<h3>` element for the title by default. Update the heading level based on what is appropriate for your page hierarchy by setting `title_tag`.
7
7
  # <%= link_to_heading_practices %>
8
8
  class BlankslateComponent < Primer::Component
9
- status :beta
9
+ status :deprecated
10
10
 
11
11
  # Optional Spinner.
12
12
  #
@@ -16,8 +16,8 @@ module Primer
16
16
 
17
17
  private
18
18
 
19
- def force_system_arguments?
20
- Rails.application.config.primer_view_components.force_system_arguments
19
+ def raise_on_invalid_options?
20
+ Rails.application.config.primer_view_components.raise_on_invalid_options
21
21
  end
22
22
 
23
23
  def deprecated_component_warning(new_class: nil, version: nil)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ # Use `HellipButton` to render a button with a hellip. Often used for hidden text expanders.
5
+ # @accessibility
6
+ # Always set an accessible label to help the user interact with the component.
7
+ #
8
+ # * This button is displaying a hellip as its content (The three dots character). Therefore a label is needed for screen readers.
9
+ # * Set the attribute `aria-label` on the system arguments. E.g. `Primer::HellipButton.new("aria-label": "Expand next part")`
10
+ class HellipButton < Primer::Component
11
+ # @example Default
12
+ # <%= render(Primer::HellipButton.new("aria-label": "No effect")) %>
13
+ #
14
+ # @example Inline
15
+ # <%= render(Primer::HellipButton.new(inline: true, "aria-label": "No effect")) %>
16
+ #
17
+ # @example Styling the button
18
+ # <%= render(Primer::HellipButton.new(p: 1, classes: "custom-class", "aria-label": "No effect")) %>
19
+ #
20
+ # @param inline [Boolean] Whether or not the button is inline.
21
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
22
+ def initialize(inline: false, **system_arguments)
23
+ @system_arguments = system_arguments
24
+
25
+ validate_aria_label
26
+
27
+ @system_arguments[:tag] = :button
28
+ @system_arguments[:"aria-expanded"] = false
29
+ @system_arguments[:classes] = class_names(
30
+ @system_arguments[:classes],
31
+ "inline" => inline
32
+ )
33
+ end
34
+
35
+ def call
36
+ render(Primer::BaseButton.new(**@system_arguments)) { "&hellip;".html_safe }
37
+ end
38
+ end
39
+ end
@@ -2,21 +2,29 @@
2
2
 
3
3
  module Primer
4
4
  # Use `HiddenTextExpander` to indicate and toggle hidden text.
5
+ #
6
+ # @accessibility
7
+ # `HiddenTextExpander` requires an `aria-label`, which will provide assistive technologies with an accessible label.
8
+ # The `aria-label` should describe the action to be invoked by the `HiddenTextExpander`. For instance,
9
+ # if your `HiddenTextExpander` expands a list of 5 comments, the `aria-label` should be
10
+ # `"Expand 5 more comments"` instead of `"More"`.
5
11
  class HiddenTextExpander < Primer::Component
6
12
  # @example Default
7
- # <%= render(Primer::HiddenTextExpander.new) %>
13
+ # <%= render(Primer::HiddenTextExpander.new("aria-label": "No effect")) %>
8
14
  #
9
15
  # @example Inline
10
- # <%= render(Primer::HiddenTextExpander.new(inline: true)) %>
16
+ # <%= render(Primer::HiddenTextExpander.new(inline: true, "aria-label": "No effect")) %>
11
17
  #
12
18
  # @example Styling the button
13
- # <%= render(Primer::HiddenTextExpander.new(button_arguments: { p: 1, classes: "custom-class" })) %>
19
+ # <%= render(Primer::HiddenTextExpander.new("aria-label": "No effect", button_arguments: { p: 1, classes: "custom-class" })) %>
14
20
  #
15
21
  # @param inline [Boolean] Whether or not the expander is inline.
16
22
  # @param button_arguments [Hash] <%= link_to_system_arguments_docs %> for the button element.
17
23
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
18
24
  def initialize(inline: false, button_arguments: {}, **system_arguments)
19
25
  @system_arguments = system_arguments
26
+ @button_arguments = button_arguments
27
+
20
28
  @system_arguments[:tag] = :span
21
29
  @system_arguments[:classes] = class_names(
22
30
  "hidden-text-expander",
@@ -24,8 +32,12 @@ module Primer
24
32
  "inline" => inline
25
33
  )
26
34
 
27
- @button_arguments = button_arguments
28
- @button_arguments[:"aria-expanded"] = false
35
+ aria_label = system_arguments[:"aria-label"] || system_arguments.dig(:aria, :label) || @aria_label
36
+ if aria_label.present?
37
+ @button_arguments[:"aria-label"] = aria_label
38
+ @system_arguments[:aria]&.delete(:label)
39
+ end
40
+
29
41
  @button_arguments[:classes] = class_names(
30
42
  "ellipsis-expander",
31
43
  button_arguments[:classes]
@@ -34,7 +46,7 @@ module Primer
34
46
 
35
47
  def call
36
48
  render(Primer::BaseComponent.new(**@system_arguments)) do
37
- render(Primer::BaseButton.new(**@button_arguments)) { "&hellip;".html_safe }
49
+ render(Primer::HellipButton.new(**@button_arguments))
38
50
  end
39
51
  end
40
52
  end
@@ -117,7 +117,7 @@ module Primer
117
117
  @system_arguments[:classes] =
118
118
  class_names(
119
119
  @system_arguments[:classes],
120
- "Subhead hx_Subhead--responsive",
120
+ "Subhead",
121
121
  "Subhead--spacious": spacious,
122
122
  "border-bottom-0": hide_border
123
123
  )
@@ -105,7 +105,7 @@ module Primer
105
105
  def validated_class_names(classes)
106
106
  return if classes.blank?
107
107
 
108
- if force_system_arguments? && !ENV["PRIMER_WARNINGS_DISABLED"]
108
+ if raise_on_invalid_options? && !ENV["PRIMER_WARNINGS_DISABLED"]
109
109
  invalid_class_names =
110
110
  classes.split.each_with_object([]) do |class_name, memo|
111
111
  memo << class_name if Primer::Classify::Validation.invalid?(class_name)
@@ -211,8 +211,8 @@ module Primer
211
211
  end
212
212
  end
213
213
 
214
- def force_system_arguments?
215
- Rails.application.config.primer_view_components.force_system_arguments
214
+ def raise_on_invalid_options?
215
+ Rails.application.config.primer_view_components.raise_on_invalid_options
216
216
  end
217
217
  end
218
218
 
@@ -15,7 +15,7 @@ module Primer
15
15
 
16
16
  config.primer_view_components = ActiveSupport::OrderedOptions.new
17
17
 
18
- config.primer_view_components.force_system_arguments = false
18
+ config.primer_view_components.raise_on_invalid_options = false
19
19
  config.primer_view_components.silence_deprecations = false
20
20
  config.primer_view_components.validate_class_names = !Rails.env.production?
21
21
 
@@ -4,6 +4,8 @@ require "json"
4
4
  require "openssl"
5
5
  require "primer/view_components/constants"
6
6
 
7
+ require_relative "tag_tree_helpers"
8
+
7
9
  # :nocov:
8
10
 
9
11
  module ERBLint
@@ -14,11 +16,7 @@ module ERBLint
14
16
  # * `CLASSES` - optional - The CSS classes that the component needs. The linter will only match elements with one of those classes.
15
17
  # * `REQUIRED_ARGUMENTS` - optional - A list of HTML attributes that are required by the component.
16
18
  class BaseLinter < Linter
17
- # from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
18
- SELF_CLOSING_TAGS = %w[
19
- area base br col command embed hr input keygen
20
- link menuitem meta param source track wbr img
21
- ].freeze
19
+ include TagTreeHelpers
22
20
 
23
21
  DUMP_FILE = ".erblint-counter-ignore.json"
24
22
  DISALLOWED_CLASSES = [].freeze
@@ -136,53 +134,6 @@ module ERBLint
136
134
  end
137
135
  end
138
136
 
139
- # This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
140
- # From the tags, we build a structured tree which represents the tag hierarchy.
141
- # With this, we are able to know where the tags start and end.
142
- def build_tag_tree(processed_source)
143
- nodes = processed_source.ast.children
144
- tag_tree = {}
145
- tags = []
146
- current_opened_tag = nil
147
-
148
- nodes.each do |node|
149
- if node.type == :tag
150
- # get the tag from previously calculated list so the references are the same
151
- tag = BetterHtml::Tree::Tag.from_node(node)
152
- tags << tag
153
-
154
- if tag.closing?
155
- if current_opened_tag && tag.name == current_opened_tag.name
156
- tag_tree[current_opened_tag][:closing] = tag
157
- current_opened_tag = tag_tree[current_opened_tag][:parent]
158
- end
159
-
160
- next
161
- end
162
-
163
- self_closing = self_closing?(tag)
164
-
165
- tag_tree[tag] = {
166
- tag: tag,
167
- closing: self_closing ? tag : nil,
168
- parent: current_opened_tag,
169
- children: []
170
- }
171
-
172
- tag_tree[current_opened_tag][:children] << tag_tree[tag] if current_opened_tag
173
- current_opened_tag = tag unless self_closing
174
- elsif current_opened_tag
175
- tag_tree[current_opened_tag][:children] << node
176
- end
177
- end
178
-
179
- [tags, tag_tree]
180
- end
181
-
182
- def self_closing?(tag)
183
- tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
184
- end
185
-
186
137
  def tags(processed_source)
187
138
  processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
188
139
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/indent"
4
+ require_relative "helpers/rubocop_helpers"
5
+
6
+ module ERBLint
7
+ module Linters
8
+ # Migrates from `Primer::BlankslateComponent` to `Primer::Beta::Blankslate`.
9
+ class BlankslateApiMigration < Linter
10
+ include ERBLint::LinterRegistry
11
+ include Helpers::RubocopHelpers
12
+
13
+ def run(processed_source)
14
+ processed_source.ast.descendants(:erb).each do |erb_node|
15
+ _, _, code_node = *erb_node
16
+ code = code_node.children.first.strip
17
+
18
+ next unless code.include?("Primer::BlankslateComponent")
19
+ # Don't fix custom blankslates
20
+ next if code.end_with?("do")
21
+
22
+ line = erb_node.loc.source_line
23
+ indent = line.split("<%=").first.size
24
+
25
+ ast = erb_ast(code)
26
+ kwargs = ast.arguments.first.arguments.last
27
+
28
+ replacement = build_replacement_blankslate(kwargs, indent)
29
+
30
+ add_offense(processed_source.to_source_range(erb_node.loc), "`Primer::BlankslateComponent` is deprecated. `Primer::Beta::Blankslate` should be used instead", replacement)
31
+ end
32
+ end
33
+
34
+ def autocorrect(_, offense)
35
+ return unless offense.context
36
+
37
+ lambda do |corrector|
38
+ corrector.replace(offense.source_range, offense.context)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def build_blankslate_arguments(kwargs)
45
+ new_blankslate = {
46
+ arguments: {},
47
+ slots: {
48
+ visual_icon: {},
49
+ visual_image: {},
50
+ heading: {
51
+ tag: ":h2"
52
+ },
53
+ description: {},
54
+ primary_action: {},
55
+ secondary_action: {}
56
+ }
57
+ }
58
+
59
+ kwargs&.pairs&.each do |pair|
60
+ source_value = pair.value.source
61
+
62
+ case pair.key.value.to_sym
63
+ when :title
64
+ new_blankslate[:slots][:heading][:content] = pair.value.value
65
+ when :title_tag
66
+ new_blankslate[:slots][:heading][:tag] = source_value
67
+ when :icon
68
+ new_blankslate[:slots][:visual_icon][:icon] = source_value
69
+ when :icon_size
70
+ new_blankslate[:slots][:visual_icon][:size] = source_value
71
+ when :image_src
72
+ new_blankslate[:slots][:visual_image][:src] = source_value
73
+ when :image_alt
74
+ new_blankslate[:slots][:visual_image][:alt] = source_value
75
+ when :description
76
+ new_blankslate[:slots][:description][:content] = pair.value.value
77
+ when :button_text
78
+ new_blankslate[:slots][:primary_action][:content] = pair.value.value
79
+ when :button_url
80
+ new_blankslate[:slots][:primary_action][:href] = source_value
81
+ when :button_classes
82
+ new_blankslate[:slots][:primary_action][:classes] = source_value
83
+ when :link_text
84
+ new_blankslate[:slots][:secondary_action][:content] = pair.value.value
85
+ when :link_url
86
+ new_blankslate[:slots][:secondary_action][:href] = source_value
87
+ when :large
88
+ next # Large does not exist anymore
89
+ else
90
+ new_blankslate[:arguments][pair.key.source] = source_value
91
+ end
92
+ end
93
+
94
+ new_blankslate
95
+ end
96
+
97
+ def build_replacement_blankslate(kwargs, indent)
98
+ data = build_blankslate_arguments(kwargs)
99
+ component_args = args_to_s(data[:arguments])
100
+
101
+ # If Blankslate has no heading, we don't update it.
102
+ return if data[:slots][:heading][:content].nil?
103
+ # If Blankslate sets both image and icon. don't update it.
104
+ return if data[:slots][:visual_icon].present? && data[:slots][:visual_image].present?
105
+
106
+ slots = data[:slots].map do |slot, slot_data|
107
+ next if slot_data.empty?
108
+
109
+ slot_args = args_to_s(slot_data.except(:content))
110
+ content = slot_data[:content]
111
+
112
+ if content
113
+ <<~HTML.indent(2)
114
+ <% c.#{slot}#{slot_args} do %>
115
+ #{content}
116
+ <% end %>
117
+ HTML
118
+ else
119
+ <<~HTML.indent(2)
120
+ <% c.#{slot}#{slot_args} %>
121
+ HTML
122
+ end
123
+ end.compact.join("\n").chomp
124
+
125
+ # Body needs to match the file indentation.
126
+ body = <<~HTML.indent(indent).chomp
127
+ #{slots}
128
+ <% end %>
129
+ HTML
130
+
131
+ # The render call will always be aligned.
132
+ "<%= render Primer::Beta::Blankslate.new#{component_args} do |c| %>\n#{body}"
133
+ end
134
+
135
+ def args_to_s(args)
136
+ string_args = args.except(:__polymorphic_type).map { |k, v| "#{k}: #{v}" }.join(", ")
137
+
138
+ string_args = ":#{args[:__polymorphic_type]}, #{string_args}" if args[:__polymorphic_type]
139
+
140
+ return string_args if string_args.blank?
141
+
142
+ "(#{string_args})"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -6,7 +6,7 @@ module ERBLint
6
6
  module Linters
7
7
  # Counts the number of times a HTML Blankslate is used instead of the component.
8
8
  class BlankslateComponentMigrationCounter < BaseLinter
9
- MESSAGE = "We are migrating Blankslate to use [Primer::BlankslateComponent](https://primer.style/view-components/components/blankslate), please try to use that instead of raw HTML."
9
+ MESSAGE = "We are migrating Blankslate to use [Primer::Beta::Blankslate](https://primer.style/view-components/components/beta/blankslate), please try to use that instead of raw HTML."
10
10
  CLASSES = %w[blankslate].freeze
11
11
  TAGS = %w[div].freeze
12
12
  end
@@ -3,12 +3,14 @@
3
3
  require_relative "base_linter"
4
4
  require_relative "autocorrectable"
5
5
  require_relative "argument_mappers/close_button"
6
+ require_relative "helpers/rubocop_helpers"
6
7
 
7
8
  module ERBLint
8
9
  module Linters
9
10
  # Counts the number of times a HTML clipboard-copy is used instead of the component.
10
11
  class CloseButtonComponentMigrationCounter < BaseLinter
11
12
  include Autocorrectable
13
+ include Helpers::RubocopHelpers
12
14
 
13
15
  TAGS = %w[button].freeze
14
16
  CLASSES = %w[close-button].freeze
@@ -109,10 +111,6 @@ module ERBLint
109
111
  (kwargs.keys.map { |key| key.value.to_s } - ALLOWED_OCTICON_ARGS).present?
110
112
  end
111
113
 
112
- def erb_ast(code)
113
- RuboCop::AST::ProcessedSource.new(code, RUBY_VERSION.to_f).ast
114
- end
115
-
116
114
  def icon(args)
117
115
  return args.first.value.to_sym if args.first.type == :sym || args.first.type == :str
118
116
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ module Helpers
6
+ # Provides helpers related to RuboCop.
7
+ module RubocopHelpers
8
+ def erb_ast(code)
9
+ RuboCop::AST::ProcessedSource.new(code, RUBY_VERSION.to_f).ast
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ # Helpers used by linters to organize HTML tags into abstract syntax trees.
6
+ module TagTreeHelpers
7
+ # from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
8
+ SELF_CLOSING_TAGS = %w[
9
+ area base br col command embed hr input keygen
10
+ link menuitem meta param source track wbr img
11
+ ].freeze
12
+
13
+ # This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
14
+ # From the tags, we build a structured tree which represents the tag hierarchy.
15
+ # With this, we are able to know where the tags start and end.
16
+ def build_tag_tree(processed_source)
17
+ nodes = processed_source.ast.children
18
+ tag_tree = {}
19
+ tags = []
20
+ current_opened_tag = nil
21
+
22
+ nodes.each do |node|
23
+ if node.type == :tag
24
+ # get the tag from previously calculated list so the references are the same
25
+ tag = BetterHtml::Tree::Tag.from_node(node)
26
+ tags << tag
27
+
28
+ if tag.closing?
29
+ if current_opened_tag && tag.name == current_opened_tag.name
30
+ tag_tree[current_opened_tag][:closing] = tag
31
+ current_opened_tag = tag_tree[current_opened_tag][:parent]
32
+ end
33
+
34
+ next
35
+ end
36
+
37
+ self_closing = self_closing?(tag)
38
+
39
+ tag_tree[tag] = {
40
+ tag: tag,
41
+ closing: self_closing ? tag : nil,
42
+ parent: current_opened_tag,
43
+ children: []
44
+ }
45
+
46
+ tag_tree[current_opened_tag][:children] << tag_tree[tag] if current_opened_tag
47
+ current_opened_tag = tag unless self_closing
48
+ elsif current_opened_tag
49
+ tag_tree[current_opened_tag][:children] << node
50
+ end
51
+ end
52
+
53
+ [tags, tag_tree]
54
+ end
55
+
56
+ def self_closing?(tag)
57
+ tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_linter"
4
+ require_relative "tag_tree_helpers"
5
+
6
+ module ERBLint
7
+ module Linters
8
+ # Counts the number of times a two column layout using col-* CSS classes is used instead of the layout component.
9
+ class TwoColumnLayoutMigrationCounter < BaseLinter
10
+ include LinterRegistry
11
+ include TagTreeHelpers
12
+
13
+ WIDTH_RANGE = (8..10).freeze
14
+ SIDEBAR_WIDTH_RANGE = (2..4).freeze
15
+
16
+ CONTAINER_CLASSES = %w[container-xl container-lg container-md container-sm].freeze
17
+ MESSAGE = "We are migrating two-column layouts to use "\
18
+ "[Primer::Alpha::Layout](https://primer.style/view-components/components/layout), "\
19
+ "please use that instead of raw HTML."
20
+
21
+ # :nodoc:
22
+ class Breakpoints
23
+ LABELS = %i[all sm md lg xl].freeze
24
+
25
+ def initialize
26
+ @map = {}
27
+ end
28
+
29
+ def set(breakpoint, value)
30
+ @map[breakpoint] = value
31
+ end
32
+
33
+ def min
34
+ LABELS.find { |label| @map[label] } || :all
35
+ end
36
+
37
+ def min_value
38
+ @map[min]
39
+ end
40
+ end
41
+
42
+ # :nodoc:
43
+ class Column
44
+ attr_reader :widths, :tag_tree
45
+
46
+ def initialize(widths, tag_tree)
47
+ @widths = widths
48
+ @tag_tree = tag_tree
49
+ end
50
+ end
51
+
52
+ # :nodoc:
53
+ class Container
54
+ attr_reader :columns
55
+
56
+ def initialize(columns)
57
+ @columns = columns
58
+ end
59
+
60
+ def sidebar
61
+ sorted_columns.first
62
+ end
63
+
64
+ def main
65
+ sorted_columns.last
66
+ end
67
+
68
+ private
69
+
70
+ def sorted_columns
71
+ @sorted_columns ||= columns.sort_by do |col|
72
+ col.widths.min_value || 0
73
+ end
74
+ end
75
+ end
76
+
77
+ def run(processed_source)
78
+ @total_offenses = 0
79
+ @offenses_not_corrected = 0
80
+
81
+ tags, tag_tree = build_tag_tree(processed_source)
82
+
83
+ tags.each do |tag|
84
+ next if tag.closing?
85
+ next unless tag.name == "div"
86
+
87
+ classes = classes_from(tag)
88
+ next if (CONTAINER_CLASSES & classes).empty?
89
+
90
+ next unless metadata_from(tag_tree[tag])
91
+
92
+ @total_offenses += 1
93
+ @offenses_not_corrected += 1
94
+
95
+ generate_offense(self.class, processed_source, tag, MESSAGE)
96
+ end
97
+
98
+ counter_correct?(processed_source)
99
+ end
100
+
101
+ private
102
+
103
+ def metadata_from(tag_tree)
104
+ tags = tag_tree[:children].select { |c| c.is_a?(Hash) }
105
+
106
+ if d_flex?(tags)
107
+ container_from(tags.first)
108
+ else
109
+ container_from(tag_tree)
110
+ end
111
+ end
112
+
113
+ def d_flex?(tags)
114
+ tags.size == 1 && classes_from(tags.first[:tag]).include?("d-flex")
115
+ end
116
+
117
+ def container_from(columns_tag_tree)
118
+ columns = columns_from(columns_tag_tree)
119
+ return unless columns.size == 2
120
+
121
+ container = Container.new(columns)
122
+
123
+ main_min = container.main.widths.min_value
124
+ sidebar_min = container.sidebar.widths.min_value
125
+ return unless sidebar_min && main_min
126
+ return unless WIDTH_RANGE.include?(main_min)
127
+ return unless SIDEBAR_WIDTH_RANGE.include?(sidebar_min)
128
+
129
+ container
130
+ end
131
+
132
+ def columns_from(tag_tree)
133
+ tag_tree[:children].each_with_object([]) do |tag_data, tags_memo|
134
+ next unless tag_data.is_a?(Hash)
135
+ next unless tag_data[:tag].name == "div"
136
+
137
+ classes = classes_from(tag_data[:tag])
138
+ widths = Breakpoints.new
139
+
140
+ classes.each do |cls|
141
+ match = cls.match(/\Acol(?:-(xl|lg|md|sm))?-(\d{1,2})(?:-max)?\z/)
142
+ next unless match
143
+
144
+ breakpoint, width = match.captures
145
+ breakpoint ||= :all
146
+ widths.set(breakpoint.to_sym, width.to_i)
147
+ end
148
+
149
+ tags_memo << Column.new(widths, tag_data)
150
+ end
151
+ end
152
+
153
+ def classes_from(tag)
154
+ tag.attributes["class"]&.value&.split(" ") || []
155
+ end
156
+ end
157
+ end
158
+ end
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 60
8
+ PATCH = 61
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end