primer_view_components 0.0.51 → 0.0.52

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/app/components/primer/beta/avatar_stack.rb +9 -9
  4. data/app/components/primer/beta/truncate.html.erb +5 -0
  5. data/app/components/primer/beta/truncate.rb +110 -0
  6. data/app/components/primer/border_box_component.rb +27 -1
  7. data/app/components/primer/clipboard_copy.rb +1 -1
  8. data/app/components/primer/dropdown.rb +7 -7
  9. data/app/components/primer/icon_button.rb +1 -1
  10. data/app/components/primer/navigation/tab_component.rb +1 -1
  11. data/app/components/primer/progress_bar_component.rb +0 -3
  12. data/app/lib/primer/fetch_or_fallback_helper.rb +2 -0
  13. data/app/lib/primer/tabbed_component_helper.rb +1 -1
  14. data/app/lib/primer/view_helper.rb +1 -0
  15. data/lib/primer/classify.rb +4 -6
  16. data/lib/primer/classify/flex.rb +1 -1
  17. data/lib/primer/classify/functional_colors.rb +1 -1
  18. data/lib/primer/classify/utilities.rb +16 -1
  19. data/lib/primer/classify/validation.rb +18 -0
  20. data/lib/primer/view_components/linters/argument_mappers/base.rb +33 -7
  21. data/lib/primer/view_components/linters/argument_mappers/button.rb +5 -6
  22. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +1 -2
  23. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +48 -5
  24. data/lib/primer/view_components/linters/argument_mappers/label.rb +3 -4
  25. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +5 -7
  26. data/lib/primer/view_components/linters/autocorrectable.rb +2 -2
  27. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +1 -1
  28. data/lib/primer/view_components/linters/helpers.rb +8 -4
  29. data/lib/primer/view_components/version.rb +1 -1
  30. data/lib/rubocop/config/default.yml +5 -0
  31. data/lib/rubocop/cop/primer.rb +1 -2
  32. data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
  33. data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
  34. data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
  35. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +3 -17
  36. data/lib/tasks/coverage.rake +4 -0
  37. data/lib/tasks/docs.rake +3 -2
  38. data/lib/tasks/utilities.rake +5 -3
  39. data/lib/yard/docs_helper.rb +4 -3
  40. data/static/arguments.yml +7 -0
  41. data/static/classes.yml +8 -0
  42. data/static/constants.json +13 -1
  43. data/static/statuses.json +3 -1
  44. metadata +9 -4
@@ -89,7 +89,7 @@ module Primer
89
89
  def justify_content(value, breakpoint)
90
90
  val = fetch_or_fallback(JUSTIFY_CONTENT_VALUES, value)
91
91
 
92
- formatted_value = val.to_s.gsub(/(flex\_|space\_)/, "")
92
+ formatted_value = val.to_s.gsub(/(flex_|space_)/, "")
93
93
  "flex#{breakpoint}-justify-#{formatted_value}"
94
94
  end
95
95
 
@@ -24,9 +24,9 @@ module Primer
24
24
  value:,
25
25
  mappings:,
26
26
  non_functional_prefix:,
27
+ functional_options:,
27
28
  functional_prefix: "",
28
29
  number_prefix: "",
29
- functional_options:,
30
30
  options_without_mappigs: []
31
31
  )
32
32
  sym_value = value.to_sym
@@ -85,7 +85,7 @@ module Primer
85
85
  return { classes: classes } if ENV["RAILS_ENV"] == "production"
86
86
 
87
87
  obj = {}
88
- classes = classes.split(" ")
88
+ classes = classes.split
89
89
  # Loop through all classes supplied and reject ones we find a match for
90
90
  # So when we're at the end of the loop we have classes left with any non-system classes.
91
91
  classes.reject! do |classname|
@@ -114,6 +114,21 @@ module Primer
114
114
  obj
115
115
  end
116
116
 
117
+ def classes_to_args(classes)
118
+ classes_to_hash(classes).map do |key, value|
119
+ val = case value
120
+ when Symbol
121
+ ":#{value}"
122
+ when String
123
+ value.to_json
124
+ else
125
+ value
126
+ end
127
+
128
+ "#{key}: #{val}"
129
+ end.join(", ")
130
+ end
131
+
117
132
  private
118
133
 
119
134
  def find_selector(selector)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utilities"
4
+
5
+ module Primer
6
+ class Classify
7
+ # :nodoc:
8
+ class Validation
9
+ INVALID_CLASS_NAME_PREFIXES = /bg-|color-|text-|box-shadow-|text-|box_shadow-/.freeze
10
+
11
+ class << self
12
+ def invalid?(class_name)
13
+ class_name.start_with?(INVALID_CLASS_NAME_PREFIXES) || Primer::Classify::Utilities.supported_selector?(class_name)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "primer/view_components/constants"
4
4
  require "primer/classify/utilities"
5
+ require "primer/classify/validation"
5
6
  require_relative "conversion_error"
6
7
  require_relative "system_arguments"
7
8
  require_relative "helpers/erb_block"
@@ -46,18 +47,37 @@ module ERBLint
46
47
 
47
48
  def attribute_to_args(attribute); end
48
49
 
49
- def map_classes(classes)
50
- system_arguments = system_arguments_to_args(classes.value)
50
+ def map_classes(classes_node)
51
+ erb_helper.raise_if_erb_block(classes_node)
52
+
53
+ system_arguments = system_arguments_to_args(classes_node.value)
51
54
  args = classes_to_args(system_arguments[:classes])
52
55
 
53
- args.merge(system_arguments.except(:classes))
56
+ invalid_classes = args[:classes].select { |class_name| Primer::Classify::Validation.invalid?(class_name) }
57
+
58
+ raise ConversionError, "Cannot convert #{'class'.pluralize(invalid_classes.size)} #{invalid_classes.join(',')}" if invalid_classes.present?
59
+
60
+ # Using splat to order the arguments in Component's args -> System Args -> custom classes
61
+ res = {
62
+ **args.except(:classes),
63
+ **system_arguments.except(:classes)
64
+ }
65
+
66
+ if args[:classes].present?
67
+ res = {
68
+ **res,
69
+ classes: args[:classes].join(" ").to_json
70
+ }
71
+ end
72
+
73
+ res
54
74
  end
55
75
 
56
- # Override this with your component's mappings
76
+ # Override this with your component's mappings, it should return a hash with the component's arguments,
77
+ # including a `classes` key that will contain all classes that the mapper couldn't handle.
78
+ # @returns { classes: Array, ... }
57
79
  def classes_to_args(classes)
58
- raise ConversionError, "Cannot convert classes `#{classes}`" if classes.present?
59
-
60
- {}
80
+ { classes: classes&.split(" ") || [] }
61
81
  end
62
82
 
63
83
  def system_arguments_to_args(classes)
@@ -68,6 +88,12 @@ module ERBLint
68
88
  v.is_a?(Symbol) ? ":#{v}" : v
69
89
  end
70
90
  end
91
+
92
+ private
93
+
94
+ def erb_helper
95
+ @erb_helper ||= Helpers::ErbBlock.new
96
+ end
71
97
  end
72
98
  end
73
99
  end
@@ -7,8 +7,6 @@ module ERBLint
7
7
  module ArgumentMappers
8
8
  # Maps classes in a button element to arguments for the Button component.
9
9
  class Button < Base
10
- require "pry"
11
-
12
10
  SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
13
11
  component: "Primer::ButtonComponent",
14
12
  constant: "SCHEME_MAPPINGS",
@@ -35,9 +33,10 @@ module ERBLint
35
33
  def attribute_to_args(attribute)
36
34
  attr_name = attribute.name
37
35
 
38
- if attr_name == "disabled"
36
+ case attr_name
37
+ when "disabled"
39
38
  { disabled: true }
40
- elsif attr_name == "type"
39
+ when "type"
41
40
  # button is the default type, so we don't need to do anything.
42
41
  return {} if attribute.value == "button"
43
42
 
@@ -48,7 +47,7 @@ module ERBLint
48
47
  end
49
48
 
50
49
  def classes_to_args(classes)
51
- classes.split(" ").each_with_object({}) do |class_name, acc|
50
+ classes.split.each_with_object({ classes: [] }) do |class_name, acc|
52
51
  next if class_name == "btn"
53
52
 
54
53
  if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
@@ -60,7 +59,7 @@ module ERBLint
60
59
  elsif class_name == "BtnGroup-item"
61
60
  acc[:group_item] = true
62
61
  else
63
- raise ConversionError, "Cannot convert class \"#{class_name}\""
62
+ acc[:classes] << class_name
64
63
  end
65
64
  end
66
65
  end
@@ -11,8 +11,7 @@ module ERBLint
11
11
  ATTRIBUTES = %w[value].freeze
12
12
 
13
13
  def attribute_to_args(attribute)
14
- Helpers::ErbBlock.raise_if_erb_block(attribute)
15
- { value: attribute.value.to_json }
14
+ { value: erb_helper.convert(attribute) }
16
15
  end
17
16
  end
18
17
  end
@@ -8,15 +8,58 @@ module ERBLint
8
8
  module Helpers
9
9
  # provides helpers to identify and deal with ERB blocks.
10
10
  class ErbBlock
11
- class << self
12
- def raise_if_erb_block(attribute)
13
- raise ERBLint::Linters::ArgumentMappers::ConversionError, "Cannot convert attribute \"#{attribute.name}\" because its value contains an erb block" if any?(attribute)
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
14
24
  end
25
+ end
15
26
 
16
- def any?(attribute)
17
- attribute.value_node&.children&.any? { |n| n.try(:type) == :erb }
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 }
18
33
  end
19
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
+ # wrap the result in `""` so it is printed as a string
61
+ "\"#{attribute.value.gsub('<%=', '#{').gsub('%>', '}')}\""
62
+ end
20
63
  end
21
64
  end
22
65
  end
@@ -27,12 +27,11 @@ module ERBLint
27
27
  ATTRIBUTES = %w[title].freeze
28
28
 
29
29
  def attribute_to_args(attribute)
30
- Helpers::ErbBlock.raise_if_erb_block(attribute)
31
- { title: attribute.value.to_json }
30
+ { title: erb_helper.convert(attribute) }
32
31
  end
33
32
 
34
33
  def classes_to_args(classes)
35
- classes.split(" ").each_with_object({}) do |class_name, acc|
34
+ classes.split.each_with_object({ classes: [] }) do |class_name, acc|
36
35
  next if class_name == "Label"
37
36
 
38
37
  if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
@@ -40,7 +39,7 @@ module ERBLint
40
39
  elsif VARIANT_MAPPINGS[class_name] && acc[:variant].nil?
41
40
  acc[:variant] = VARIANT_MAPPINGS[class_name]
42
41
  else
43
- raise ConversionError, "Cannot convert class \"#{class_name}\""
42
+ acc[:classes] << class_name
44
43
  end
45
44
  end
46
45
  end
@@ -11,9 +11,11 @@ module ERBLint
11
11
  STRING_PARAMETERS = %w[aria- data-].freeze
12
12
  TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
13
13
 
14
- attr_reader :attribute
14
+ attr_reader :attribute, :erb_helper
15
+
15
16
  def initialize(attribute)
16
17
  @attribute = attribute
18
+ @erb_helper = Helpers::ErbBlock.new
17
19
  end
18
20
 
19
21
  def to_args
@@ -29,13 +31,9 @@ module ERBLint
29
31
 
30
32
  { test_selector: m[:selector].tr("'", '"') }
31
33
  elsif attr_name == "data-test-selector"
32
- Helpers::ErbBlock.raise_if_erb_block(attribute)
33
-
34
- { test_selector: attribute.value.to_json }
34
+ { test_selector: erb_helper.convert(attribute) }
35
35
  elsif attr_name.start_with?(*STRING_PARAMETERS)
36
- Helpers::ErbBlock.raise_if_erb_block(attribute)
37
-
38
- { "\"#{attr_name}\"" => attribute.value.to_json }
36
+ { "\"#{attr_name}\"" => erb_helper.convert(attribute) }
39
37
  else
40
38
  raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
41
39
  end
@@ -20,10 +20,10 @@ module ERBLint
20
20
  "#{correction} do %>"
21
21
  end
22
22
 
23
- def message(args)
23
+ def message(args, processed_source)
24
24
  return self.class::MESSAGE if args.nil?
25
25
 
26
- "#{self.class::MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
26
+ "#{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
27
  end
28
28
  end
29
29
  end
@@ -12,7 +12,7 @@ module ERBLint
12
12
  include Autocorrectable
13
13
 
14
14
  TAGS = %w[clipboard-copy].freeze
15
- CLASSES = %w[].freeze
15
+ REQUIRED_ARGUMENTS = [/for|value/, "aria-label"].freeze
16
16
  MESSAGE = "We are migrating clipboard-copy to use [Primer::ClipboardCopy](https://primer.style/view-components/components/clipboardcopy), please try to use that instead of raw HTML."
17
17
  ARGUMENT_MAPPER = ArgumentMappers::ClipboardCopy
18
18
  COMPONENT = "Primer::ClipboardCopy"
@@ -15,6 +15,8 @@ module ERBLint
15
15
  ].freeze
16
16
 
17
17
  DUMP_FILE = ".erblint-counter-ignore.json"
18
+ CLASSES = [].freeze
19
+ REQUIRED_ARGUMENTS = [].freeze
18
20
 
19
21
  def self.included(base)
20
22
  base.include(ERBLint::LinterRegistry)
@@ -31,7 +33,6 @@ module ERBLint
31
33
  next unless self.class::TAGS&.include?(tag.name)
32
34
 
33
35
  classes = tag.attributes["class"]&.value&.split(" ") || []
34
-
35
36
  tag_tree[tag][:offense] = false
36
37
 
37
38
  next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
@@ -39,9 +40,12 @@ module ERBLint
39
40
  args = map_arguments(tag)
40
41
  correction = correction(args)
41
42
 
43
+ attributes = tag.attributes.each.map(&:name).join(" ")
44
+ matches_required_attributes = self.class::REQUIRED_ARGUMENTS.blank? || self.class::REQUIRED_ARGUMENTS.all? { |arg| attributes.match?(arg) }
45
+
42
46
  tag_tree[tag][:offense] = true
43
- tag_tree[tag][:correctable] = !correction.nil?
44
- tag_tree[tag][:message] = message(args)
47
+ tag_tree[tag][:correctable] = matches_required_attributes && !correction.nil?
48
+ tag_tree[tag][:message] = message(args, processed_source)
45
49
  tag_tree[tag][:correction] = correction
46
50
  end
47
51
 
@@ -97,7 +101,7 @@ module ERBLint
97
101
  # Override this function to customize the linter message.
98
102
  #
99
103
  # @return [String] message to show on linter error.
100
- def message(_tag)
104
+ def message(_tag, _processed_source)
101
105
  self.class::MESSAGE
102
106
  end
103
107
 
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 51
8
+ PATCH = 52
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -10,3 +10,8 @@ Primer/SystemArgumentInsteadOfClass:
10
10
  Primer/NoTagMemoize:
11
11
  Enabled: false
12
12
 
13
+ Primer/PrimerOcticon:
14
+ Enabled: true
15
+
16
+ Primer/DeprecatedArguments:
17
+ Enabled: true
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop/cop/primer/no_tag_memoize"
4
- require "rubocop/cop/primer/system_argument_instead_of_class"
3
+ Dir[File.join(__dir__, "primer", "*.rb")].sort.each { |file| require file }
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "primer/view_components/statuses"
5
+ require_relative "../../../../app/lib/primer/view_helper"
6
+
7
+ # :nocov:
8
+ module RuboCop
9
+ module Cop
10
+ module Primer
11
+ # This cop ensures that components don't use deprecated arguments
12
+ #
13
+ # bad
14
+ # Component.new(foo: :deprecated)
15
+ #
16
+ # good
17
+ # Component.new(foo: :bar)
18
+ class DeprecatedArguments < RuboCop::Cop::Cop
19
+ INVALID_MESSAGE = <<~STR
20
+ Avoid using deprecated arguments: https://primer.style/view-components/deprecated.
21
+ STR
22
+
23
+ # This is a hash of deprecated arguments and their replacements.
24
+ #
25
+ # * The top level key is the argument.
26
+ # * The second level key is the value.
27
+ # * The seceond level value is a string of the full replacement. e.g. "new_argument: :new_value"
28
+ # If the value is nil, then there is no replacement.
29
+ #
30
+ # e.g.
31
+ # DEPRECATED = {
32
+ # argument: {
33
+ # value: "new_argument: :new_value"
34
+ # }
35
+ # }
36
+ #
37
+ DEPRECATED = {
38
+ color: {
39
+ blue: "color: :text_link",
40
+ gray_dark: "color: :text_primary",
41
+ gray: "color: :text_secondary",
42
+ gray_light: "color: :text_tertiary",
43
+ green: "color: :text_success",
44
+ yellow: "color: :text_warning",
45
+ red: "color: :text_danger",
46
+ gray_0: nil,
47
+ gray_1: nil,
48
+ gray_2: nil,
49
+ gray_3: nil,
50
+ gray_4: nil,
51
+ gray_5: nil,
52
+ gray_6: nil,
53
+ gray_7: nil,
54
+ gray_8: nil,
55
+ gray_9: nil,
56
+ blue_0: nil,
57
+ blue_1: nil,
58
+ blue_2: nil,
59
+ blue_3: nil,
60
+ blue_4: nil,
61
+ blue_5: nil,
62
+ blue_6: nil,
63
+ blue_7: nil,
64
+ blue_8: nil,
65
+ blue_9: nil,
66
+ green_0: nil,
67
+ green_1: nil,
68
+ green_2: nil,
69
+ green_3: nil,
70
+ green_4: nil,
71
+ green_5: nil,
72
+ green_6: nil,
73
+ green_7: nil,
74
+ green_8: nil,
75
+ green_9: nil,
76
+ yellow_0: nil,
77
+ yellow_1: nil,
78
+ yellow_2: nil,
79
+ yellow_3: nil,
80
+ yellow_4: nil,
81
+ yellow_5: nil,
82
+ yellow_6: nil,
83
+ yellow_7: nil,
84
+ yellow_8: nil,
85
+ yellow_9: nil,
86
+ red_0: nil,
87
+ red_1: nil,
88
+ red_2: nil,
89
+ red_3: nil,
90
+ red_4: nil,
91
+ red_5: nil,
92
+ red_6: nil,
93
+ red_7: nil,
94
+ red_8: nil,
95
+ red_9: nil,
96
+ purple_0: nil,
97
+ purple_1: nil,
98
+ purple_2: nil,
99
+ purple_3: nil,
100
+ purple_4: nil,
101
+ purple_5: nil,
102
+ purple_6: nil,
103
+ purple_7: nil,
104
+ purple_8: nil,
105
+ purple_9: nil,
106
+ pink_0: nil,
107
+ pink_1: nil,
108
+ pink_2: nil,
109
+ pink_3: nil,
110
+ pink_4: nil,
111
+ pink_5: nil,
112
+ pink_6: nil,
113
+ pink_7: nil,
114
+ pink_8: nil,
115
+ pink_9: nil,
116
+ orange_0: nil,
117
+ orange_1: nil,
118
+ orange_2: nil,
119
+ orange_3: nil,
120
+ orange_4: nil,
121
+ orange_5: nil,
122
+ orange_6: nil,
123
+ orange_7: nil,
124
+ orange_8: nil,
125
+ orange_9: nil
126
+ }
127
+ }.freeze
128
+
129
+ def on_send(node)
130
+ return unless valid_node?(node)
131
+ return unless node.arguments?
132
+
133
+ # we are looking for hash arguments and they are always last
134
+ kwargs = node.arguments.last
135
+
136
+ return unless kwargs.type == :hash
137
+
138
+ kwargs.pairs.each do |pair|
139
+ # Skip if we're not dealing with a symbol
140
+ next if pair.key.type != :sym
141
+ next unless pair.value.type == :sym || pair.value.type == :str
142
+
143
+ key = pair.key.value
144
+ value = pair.value.value.to_sym
145
+
146
+ next unless DEPRECATED.key?(key) && DEPRECATED[key].key?(value)
147
+
148
+ add_offense(pair, message: INVALID_MESSAGE)
149
+ end
150
+ end
151
+
152
+ def autocorrect(node)
153
+ lambda do |corrector|
154
+ replacement = DEPRECATED[node.key.value][node.value.value.to_sym]
155
+ corrector.replace(node, replacement) if replacement.present?
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ # We only verify SystemArguments if it's a `.new` call on a component or
162
+ # a ViewHleper call.
163
+ def valid_node?(node)
164
+ view_helpers.include?(node.method_name) || (node.method_name == :new && ::Primer::ViewComponents::STATUSES.key?(node.receiver.const_name))
165
+ end
166
+
167
+ def view_helpers
168
+ ::Primer::ViewHelper::HELPERS.keys.map { |key| "primer_#{key}".to_sym }
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end