primer_view_components 0.0.49 → 0.0.53

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/avatar_stack.rb +9 -9
  5. data/app/components/primer/beta/truncate.html.erb +5 -0
  6. data/app/components/primer/beta/truncate.rb +110 -0
  7. data/app/components/primer/border_box_component.rb +27 -1
  8. data/app/components/primer/clipboard_copy.html.erb +2 -2
  9. data/app/components/primer/clipboard_copy.rb +1 -1
  10. data/app/components/primer/dropdown.rb +7 -7
  11. data/app/components/primer/icon_button.rb +1 -1
  12. data/app/components/primer/label_component.rb +13 -12
  13. data/app/components/primer/navigation/tab_component.rb +1 -1
  14. data/app/components/primer/progress_bar_component.rb +0 -3
  15. data/app/components/primer/tab_nav_component.rb +1 -1
  16. data/app/lib/primer/fetch_or_fallback_helper.rb +2 -0
  17. data/app/lib/primer/octicon/cache.rb +1 -1
  18. data/app/lib/primer/tabbed_component_helper.rb +1 -1
  19. data/app/lib/primer/view_helper.rb +1 -0
  20. data/lib/primer/classify/cache.rb +0 -5
  21. data/lib/primer/classify/flex.rb +1 -1
  22. data/lib/primer/classify/functional_colors.rb +1 -1
  23. data/lib/primer/classify/utilities.rb +19 -2
  24. data/lib/primer/classify/utilities.yml +16 -0
  25. data/lib/primer/classify/validation.rb +18 -0
  26. data/lib/primer/classify.rb +4 -18
  27. data/lib/primer/view_components/constants.rb +1 -1
  28. data/lib/primer/view_components/linters/argument_mappers/base.rb +63 -2
  29. data/lib/primer/view_components/linters/argument_mappers/button.rb +7 -11
  30. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +2 -6
  31. data/lib/primer/view_components/linters/argument_mappers/close_button.rb +43 -0
  32. data/lib/primer/view_components/linters/argument_mappers/flash.rb +32 -0
  33. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +67 -0
  34. data/lib/primer/view_components/linters/argument_mappers/label.rb +5 -12
  35. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +6 -5
  36. data/lib/primer/view_components/linters/autocorrectable.rb +6 -4
  37. data/lib/primer/view_components/linters/{helpers.rb → base_linter.rb} +69 -29
  38. data/lib/primer/view_components/linters/button_component_migration_counter.rb +4 -3
  39. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +3 -4
  40. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +123 -0
  41. data/lib/primer/view_components/linters/flash_component_migration_counter.rb +18 -3
  42. data/lib/primer/view_components/linters/label_component_migration_counter.rb +2 -3
  43. data/lib/primer/view_components/version.rb +1 -1
  44. data/lib/rubocop/config/default.yml +5 -0
  45. data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
  46. data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
  47. data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
  48. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +12 -16
  49. data/lib/rubocop/cop/primer.rb +1 -2
  50. data/lib/tasks/coverage.rake +4 -0
  51. data/lib/tasks/docs.rake +3 -2
  52. data/lib/tasks/utilities.rake +7 -3
  53. data/lib/yard/docs_helper.rb +6 -3
  54. data/static/arguments.yml +7 -4
  55. data/static/classes.yml +8 -0
  56. data/static/constants.json +13 -1
  57. data/static/statuses.json +3 -1
  58. metadata +32 -9
@@ -8,16 +8,12 @@ module ERBLint
8
8
  # Maps attributes in the clipboard-copy element to arguments for the ClipboardCopy component.
9
9
  class ClipboardCopy < Base
10
10
  DEFAULT_TAG = "clipboard-copy"
11
+ ATTRIBUTES = %w[role tabindex for value id style].freeze
11
12
 
12
13
  def attribute_to_args(attribute)
13
14
  attr_name = attribute.name
14
15
 
15
- if attr_name == "value"
16
- { value: attribute.value.to_json }
17
- else
18
- # Assume the attribute is a system argument.
19
- SystemArguments.new(attribute).to_args
20
- end
16
+ { attr_name.to_sym => erb_helper.convert(attribute) }
21
17
  end
22
18
  end
23
19
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ module ArgumentMappers
8
+ # Maps classes in a close-button element to arguments for the CloseButton component.
9
+ class CloseButton < Base
10
+ ATTRIBUTES = %w[type].freeze
11
+
12
+ TYPE_OPTIONS = Primer::ViewComponents::Constants.get(
13
+ component: "Primer::CloseButton",
14
+ constant: "TYPE_OPTIONS"
15
+ ).freeze
16
+
17
+ DEFAULT_TYPE = Primer::ViewComponents::Constants.get(
18
+ component: "Primer::CloseButton",
19
+ constant: "DEFAULT_TYPE"
20
+ ).freeze
21
+
22
+ DEFAULT_CLASS = "close-button"
23
+
24
+ def attribute_to_args(attribute)
25
+ # button is the default type, so we don't need to do anything.
26
+ return {} if attribute.value == DEFAULT_TYPE
27
+
28
+ raise ConversionError, "CloseButton component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
29
+
30
+ { type: ":#{attribute.value}" }
31
+ end
32
+
33
+ def classes_to_args(classes)
34
+ classes.each_with_object({ classes: [] }) do |class_name, acc|
35
+ next if class_name == DEFAULT_CLASS
36
+
37
+ acc[:classes] << class_name
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ module ArgumentMappers
8
+ # Maps classes in a flash element to arguments for the Flash component.
9
+ class Flash < Base
10
+ SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
11
+ component: "Primer::FlashComponent",
12
+ constant: "SCHEME_MAPPINGS",
13
+ symbolize: true
14
+ ).freeze
15
+
16
+ def classes_to_args(classes)
17
+ classes.each_with_object({ classes: [] }) do |class_name, acc|
18
+ next if class_name == "flash"
19
+
20
+ if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
21
+ acc[:scheme] = SCHEME_MAPPINGS[class_name]
22
+ elsif class_name == "flash-full"
23
+ acc[:full] = true
24
+ else
25
+ acc[:classes] << class_name
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../conversion_error"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ module ArgumentMappers
8
+ module Helpers
9
+ # provides helpers to identify and deal with ERB blocks.
10
+ class ErbBlock
11
+ INTERPOLATION_REGEX = /^<%=(?<rb>.*)%>$/.freeze
12
+
13
+ def raise_if_erb_block(attribute)
14
+ raise_error(attribute) if any?(attribute)
15
+ end
16
+
17
+ def convert(attribute)
18
+ raise_error(attribute) unless interpolation?(attribute)
19
+
20
+ if any?(attribute)
21
+ convert_interpolation(attribute)
22
+ else
23
+ attribute.value.to_json
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def interpolation?(attribute)
30
+ erb_blocks(attribute).all? do |erb|
31
+ # If the blocks does not have an indicator, it's not an interpolation.
32
+ erb.children.to_a.compact.any? { |node| node.type == :indicator }
33
+ end
34
+ end
35
+
36
+ def raise_error(attribute)
37
+ raise ERBLint::Linters::ArgumentMappers::ConversionError, "Cannot convert attribute \"#{attribute.name}\" because its value contains an erb block"
38
+ end
39
+
40
+ def any?(attribute)
41
+ erb_blocks(attribute).any?
42
+ end
43
+
44
+ def basic?(attribute)
45
+ return false if erb_blocks(attribute).size != 1
46
+
47
+ attribute.value.match?(INTERPOLATION_REGEX)
48
+ end
49
+
50
+ def erb_blocks(attribute)
51
+ (attribute.value_node&.children || []).select { |n| n.try(:type) == :erb }
52
+ end
53
+
54
+ def convert_interpolation(attribute)
55
+ if basic?(attribute)
56
+ m = attribute.value.match(INTERPOLATION_REGEX)
57
+ return m[:rb].strip
58
+ end
59
+
60
+ # we use `source` instead of `value` because it does not convert encoded HTML entities.
61
+ attribute.value_node.loc.source.gsub("<%=", '#{').gsub("%>", "}")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -24,21 +24,14 @@ module ERBLint
24
24
  constant: "DEFAULT_TAG"
25
25
  ).freeze
26
26
 
27
+ ATTRIBUTES = %w[title].freeze
28
+
27
29
  def attribute_to_args(attribute)
28
- attr_name = attribute.name
29
-
30
- if attr_name == "class"
31
- classes_to_args(attribute)
32
- elsif attr_name == "title"
33
- { title: attribute.value.to_json }
34
- else
35
- # Assume the attribute is a system argument.
36
- SystemArguments.new(attribute).to_args
37
- end
30
+ { title: erb_helper.convert(attribute) }
38
31
  end
39
32
 
40
33
  def classes_to_args(classes)
41
- classes.value.split(" ").each_with_object({}) do |class_name, acc|
34
+ classes.each_with_object({ classes: [] }) do |class_name, acc|
42
35
  next if class_name == "Label"
43
36
 
44
37
  if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
@@ -46,7 +39,7 @@ module ERBLint
46
39
  elsif VARIANT_MAPPINGS[class_name] && acc[:variant].nil?
47
40
  acc[:variant] = VARIANT_MAPPINGS[class_name]
48
41
  else
49
- raise ConversionError, "Cannot convert class \"#{class_name}\""
42
+ acc[:classes] << class_name
50
43
  end
51
44
  end
52
45
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "conversion_error"
4
+ require_relative "helpers/erb_block"
4
5
 
5
6
  module ERBLint
6
7
  module Linters
@@ -10,9 +11,11 @@ module ERBLint
10
11
  STRING_PARAMETERS = %w[aria- data-].freeze
11
12
  TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
12
13
 
13
- attr_reader :attribute
14
+ attr_reader :attribute, :erb_helper
15
+
14
16
  def initialize(attribute)
15
17
  @attribute = attribute
18
+ @erb_helper = Helpers::ErbBlock.new
16
19
  end
17
20
 
18
21
  def to_args
@@ -28,11 +31,9 @@ module ERBLint
28
31
 
29
32
  { test_selector: m[:selector].tr("'", '"') }
30
33
  elsif attr_name == "data-test-selector"
31
- { test_selector: attribute.value.to_json }
34
+ { test_selector: erb_helper.convert(attribute) }
32
35
  elsif attr_name.start_with?(*STRING_PARAMETERS)
33
- raise ConversionError, "Cannot convert attribute \"#{attr_name}\" because its value contains an erb block" if attribute.value_node&.children&.any? { |n| n.try(:type) == :erb }
34
-
35
- { "\"#{attr_name}\"" => attribute.value.to_json }
36
+ { "\"#{attr_name}\"" => erb_helper.convert(attribute) }
36
37
  else
37
38
  raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
38
39
  end
@@ -4,9 +4,11 @@ require_relative "argument_mappers/conversion_error"
4
4
 
5
5
  module ERBLint
6
6
  module Linters
7
- # Helper methods for autocorrectable ERB linters.
7
+ # Provides the autocorrection functionality for the linter. Once included, you should define the following constants:
8
+ # * `ARGUMENT_MAPPER` - required - The class responsible for transforming classes and attributes into arguments for the component.
9
+ # * `COMPONENT` - required - The component name for the linter. It will be used to generate the correction.
8
10
  module Autocorrectable
9
- def map_arguments(tag)
11
+ def map_arguments(tag, _tag_tree)
10
12
  self.class::ARGUMENT_MAPPER.new(tag).to_s
11
13
  rescue ArgumentMappers::ConversionError
12
14
  nil
@@ -20,10 +22,10 @@ module ERBLint
20
22
  "#{correction} do %>"
21
23
  end
22
24
 
23
- def message(args)
25
+ def message(args, processed_source)
24
26
  return self.class::MESSAGE if args.nil?
25
27
 
26
- "#{self.class::MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
28
+ "#{self.class::MESSAGE}\nTry using:\n\n#{correction(args)}\n\nYou can also run erblint in autocorrect mode:\n\nbundle exec erblint -a #{processed_source.filename}\n"
27
29
  end
28
30
  end
29
31
  end
@@ -4,10 +4,16 @@ require "json"
4
4
  require "openssl"
5
5
  require "primer/view_components/constants"
6
6
 
7
+ # :nocov:
8
+
7
9
  module ERBLint
8
10
  module Linters
9
- # Helper methods for linting ERB.
10
- module Helpers
11
+ # Provides the basic linter logic. When inherited, you should define:
12
+ # * `TAGS` - required - The HTML tags that the component supports. It will be used by the linter to match elements.
13
+ # * `MESSAGE` - required - The message shown when there's an offense.
14
+ # * `CLASSES` - optional - The CSS classes that the component needs. The linter will only match elements with one of those classes.
15
+ # * `REQUIRED_ARGUMENTS` - optional - A list of HTML attributes that are required by the component.
16
+ class BaseLinter < Linter
11
17
  # from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
12
18
  SELF_CLOSING_TAGS = %w[
13
19
  area base br col command embed hr input keygen
@@ -15,33 +21,44 @@ module ERBLint
15
21
  ].freeze
16
22
 
17
23
  DUMP_FILE = ".erblint-counter-ignore.json"
24
+ DISALLOWED_CLASSES = [].freeze
25
+ CLASSES = [].freeze
26
+ REQUIRED_ARGUMENTS = [].freeze
27
+
28
+ class ConfigSchema < LinterConfig
29
+ property :override_ignores_if_correctable, accepts: [true, false], default: false, reader: :override_ignores_if_correctable?
30
+ end
18
31
 
19
- def self.included(base)
32
+ def self.inherited(base)
33
+ super
20
34
  base.include(ERBLint::LinterRegistry)
35
+ base.config_schema = ConfigSchema
21
36
  end
22
37
 
23
38
  def run(processed_source)
24
39
  @total_offenses = 0
25
40
  @offenses_not_corrected = 0
26
- tags = tags(processed_source)
27
- tag_tree = build_tag_tree(tags)
41
+ (tags, tag_tree) = build_tag_tree(processed_source)
28
42
 
29
43
  tags.each do |tag|
30
44
  next if tag.closing?
31
45
  next unless self.class::TAGS&.include?(tag.name)
32
46
 
33
47
  classes = tag.attributes["class"]&.value&.split(" ") || []
34
-
35
48
  tag_tree[tag][:offense] = false
36
49
 
50
+ next if (classes & self.class::DISALLOWED_CLASSES).any?
37
51
  next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
38
52
 
39
- args = map_arguments(tag)
53
+ args = map_arguments(tag, tag_tree[tag])
40
54
  correction = correction(args)
41
55
 
56
+ attributes = tag.attributes.each.map(&:name).join(" ")
57
+ matches_required_attributes = self.class::REQUIRED_ARGUMENTS.blank? || self.class::REQUIRED_ARGUMENTS.all? { |arg| attributes.match?(arg) }
58
+
42
59
  tag_tree[tag][:offense] = true
43
- tag_tree[tag][:correctable] = !correction.nil?
44
- tag_tree[tag][:message] = message(args)
60
+ tag_tree[tag][:correctable] = matches_required_attributes && !correction.nil?
61
+ tag_tree[tag][:message] = message(args, processed_source)
45
62
  tag_tree[tag][:correction] = correction
46
63
  end
47
64
 
@@ -51,8 +68,7 @@ module ERBLint
51
68
  @total_offenses += 1
52
69
  # We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
53
70
  if h[:correctable]
54
- add_offense(tag.loc, h[:message], h[:correction])
55
- add_offense(h[:closing].loc, h[:message], "<% end %>")
71
+ add_correction(tag, h)
56
72
  else
57
73
  @offenses_not_corrected += 1
58
74
  generate_offense(self.class, processed_source, tag, h[:message])
@@ -78,11 +94,16 @@ module ERBLint
78
94
 
79
95
  private
80
96
 
97
+ def add_correction(tag, tag_tree)
98
+ add_offense(tag.loc, tag_tree[:message], tag_tree[:correction])
99
+ add_offense(tag_tree[:closing].loc, tag_tree[:message], "<% end %>")
100
+ end
101
+
81
102
  # Override this function to convert the HTML element attributes to argument for a component.
82
103
  #
83
104
  # @return [Hash] if possible to map all attributes to arguments.
84
105
  # @return [Nil] if cannot map to arguments.
85
- def map_arguments(_tag)
106
+ def map_arguments(_tag, _tag_tree)
86
107
  nil
87
108
  end
88
109
 
@@ -97,7 +118,7 @@ module ERBLint
97
118
  # Override this function to customize the linter message.
98
119
  #
99
120
  # @return [String] message to show on linter error.
100
- def message(_tag)
121
+ def message(_tag, _processed_source)
101
122
  self.class::MESSAGE
102
123
  end
103
124
 
@@ -118,31 +139,44 @@ module ERBLint
118
139
  # This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
119
140
  # From the tags, we build a structured tree which represents the tag hierarchy.
120
141
  # With this, we are able to know where the tags start and end.
121
- def build_tag_tree(tags)
142
+ def build_tag_tree(processed_source)
143
+ nodes = processed_source.ast.children
122
144
  tag_tree = {}
145
+ tags = []
123
146
  current_opened_tag = nil
124
147
 
125
- tags.each do |tag|
126
- if tag.closing?
127
- if current_opened_tag && tag.name == current_opened_tag.name
128
- tag_tree[current_opened_tag][:closing] = tag
129
- current_opened_tag = tag_tree[current_opened_tag][:parent]
130
- end
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
131
153
 
132
- next
133
- end
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
134
162
 
135
- self_closing = self_closing?(tag)
163
+ self_closing = self_closing?(tag)
136
164
 
137
- tag_tree[tag] = {
138
- closing: self_closing ? tag : nil,
139
- parent: current_opened_tag
140
- }
165
+ tag_tree[tag] = {
166
+ tag: tag,
167
+ closing: self_closing ? tag : nil,
168
+ parent: current_opened_tag,
169
+ children: []
170
+ }
141
171
 
142
- current_opened_tag = tag unless self_closing
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
143
177
  end
144
178
 
145
- tag_tree
179
+ [tags, tag_tree]
146
180
  end
147
181
 
148
182
  def self_closing?(tag)
@@ -169,6 +203,12 @@ module ERBLint
169
203
  end
170
204
  end
171
205
 
206
+ # Unless explicitly set, we don't want to mark correctable offenses if the counter is correct.
207
+ if !@config.override_ignores_if_correctable? && expected_count == @total_offenses
208
+ clear_offenses
209
+ return
210
+ end
211
+
172
212
  if @offenses_not_corrected.zero?
173
213
  # have to adjust to get `\n` so we delete the whole line
174
214
  add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:count comment for #{rule_name}", "") if comment_node
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "helpers"
3
+ require_relative "base_linter"
4
4
  require_relative "autocorrectable"
5
5
  require_relative "argument_mappers/button"
6
6
 
7
7
  module ERBLint
8
8
  module Linters
9
9
  # Counts the number of times a HTML button is used instead of the component.
10
- class ButtonComponentMigrationCounter < Linter
11
- include Helpers
10
+ class ButtonComponentMigrationCounter < BaseLinter
12
11
  include Autocorrectable
13
12
 
14
13
  TAGS = Primer::ViewComponents::Constants.get(
@@ -16,6 +15,8 @@ module ERBLint
16
15
  constant: "TAG_OPTIONS"
17
16
  ).freeze
18
17
 
18
+ # CloseButton component has preference when this class is seen in conjuction with `btn`.
19
+ DISALLOWED_CLASSES = %w[close-button].freeze
19
20
  CLASSES = %w[btn btn-link].freeze
20
21
  MESSAGE = "We are migrating buttons to use [Primer::ButtonComponent](https://primer.style/view-components/components/button), please try to use that instead of raw HTML."
21
22
  ARGUMENT_MAPPER = ArgumentMappers::Button