primer_view_components 0.0.60 → 0.0.61

Sign up to get free protection for your applications and to get access to all the features.
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