primer_view_components 0.0.46 → 0.0.50

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/auto_complete.rb +159 -0
  5. data/app/components/primer/beta/auto_complete/auto_complete.d.ts +1 -0
  6. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.html.erb +0 -0
  7. data/app/components/primer/beta/auto_complete/auto_complete.js +1 -0
  8. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.ts +0 -0
  9. data/app/components/primer/beta/auto_complete/item.rb +44 -0
  10. data/app/components/primer/beta/avatar.rb +77 -0
  11. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  12. data/app/components/primer/beta/avatar_stack.rb +92 -0
  13. data/app/components/primer/clipboard_copy.html.erb +2 -2
  14. data/app/components/primer/component.rb +5 -1
  15. data/app/components/primer/details_component.rb +7 -7
  16. data/app/components/primer/image_crop.html.erb +4 -4
  17. data/app/components/primer/label_component.rb +13 -12
  18. data/app/components/primer/markdown.rb +9 -9
  19. data/app/components/primer/navigation/tab_component.rb +30 -2
  20. data/app/components/primer/popover_component.rb +6 -3
  21. data/app/components/primer/primer.d.ts +1 -1
  22. data/app/components/primer/primer.js +1 -1
  23. data/app/components/primer/primer.ts +1 -1
  24. data/app/components/primer/tab_nav_component.rb +4 -3
  25. data/app/components/primer/timeline_item_component.rb +2 -2
  26. data/app/components/primer/truncate.rb +6 -1
  27. data/app/components/primer/underline_nav_component.rb +4 -3
  28. data/app/lib/primer/octicon/cache.rb +1 -1
  29. data/lib/primer/classify.rb +4 -18
  30. data/lib/primer/classify/cache.rb +0 -5
  31. data/lib/primer/classify/utilities.rb +54 -22
  32. data/lib/primer/classify/utilities.yml +16 -0
  33. data/lib/primer/view_components.rb +34 -6
  34. data/lib/primer/view_components/constants.rb +55 -0
  35. data/lib/primer/view_components/linters/argument_mappers/base.rb +74 -0
  36. data/lib/primer/view_components/linters/argument_mappers/button.rb +32 -44
  37. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +20 -0
  38. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +24 -0
  39. data/lib/primer/view_components/linters/argument_mappers/label.rb +50 -0
  40. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +4 -1
  41. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  42. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  43. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  44. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
  45. data/lib/primer/view_components/linters/helpers.rb +56 -38
  46. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  47. data/lib/primer/view_components/statuses.rb +14 -0
  48. data/lib/primer/view_components/version.rb +1 -1
  49. data/lib/rubocop/config/default.yml +12 -0
  50. data/lib/rubocop/cop/primer.rb +4 -0
  51. data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
  52. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +75 -0
  53. data/lib/tasks/constants.rake +12 -0
  54. data/lib/tasks/docs.rake +87 -32
  55. data/lib/tasks/utilities.rake +4 -10
  56. data/lib/yard/docs_helper.rb +12 -3
  57. data/static/arguments.yml +973 -0
  58. data/static/assets/view-components.svg +18 -0
  59. data/static/classes.yml +174 -0
  60. data/static/constants.json +628 -0
  61. data/static/statuses.json +5 -5
  62. metadata +34 -13
  63. data/app/components/primer/auto_complete.rb +0 -157
  64. data/app/components/primer/auto_complete/item.rb +0 -42
  65. data/app/components/primer/avatar_component.rb +0 -75
  66. data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -1,66 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "conversion_error"
4
- require_relative "system_arguments"
3
+ require_relative "base"
5
4
 
6
5
  module ERBLint
7
6
  module Linters
8
7
  module ArgumentMappers
9
8
  # Maps classes in a button element to arguments for the Button component.
10
- class Button
11
- SCHEME_MAPPINGS = {
12
- "btn-primary" => ":primary",
13
- "btn-danger" => ":danger",
14
- "btn-outline" => ":outline",
15
- "btn-invisible" => ":invisible",
16
- "btn-link" => ":link"
17
- }.freeze
9
+ class Button < Base
10
+ require "pry"
18
11
 
19
- VARIANT_MAPPINGS = {
20
- "btn-sm" => ":small",
21
- "btn-large" => ":large"
22
- }.freeze
12
+ SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
13
+ component: "Primer::ButtonComponent",
14
+ constant: "SCHEME_MAPPINGS",
15
+ symbolize: true
16
+ ).freeze
23
17
 
24
- TYPE_OPTIONS = %w[button reset submit].freeze
18
+ VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
19
+ component: "Primer::ButtonComponent",
20
+ constant: "VARIANT_MAPPINGS",
21
+ symbolize: true
22
+ ).freeze
25
23
 
26
- def initialize(tag)
27
- @tag = tag
28
- end
29
-
30
- def to_s
31
- to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
32
- end
33
-
34
- def to_args
35
- args = {}
24
+ TYPE_OPTIONS = Primer::ViewComponents::Constants.get(
25
+ component: "Primer::BaseButton",
26
+ constant: "TYPE_OPTIONS"
27
+ ).freeze
28
+ DEFAULT_TAG = Primer::ViewComponents::Constants.get(
29
+ component: "Primer::BaseButton",
30
+ constant: "DEFAULT_TAG"
31
+ ).freeze
36
32
 
37
- args[:tag] = ":#{@tag.name}" unless @tag.name == "button"
33
+ ATTRIBUTES = %w[disabled type].freeze
38
34
 
39
- @tag.attributes.each do |attribute|
40
- attr_name = attribute.name
35
+ def attribute_to_args(attribute)
36
+ attr_name = attribute.name
41
37
 
42
- if attr_name == "class"
43
- args = args.merge(classes_to_args(attribute))
44
- elsif attr_name == "disabled"
45
- args[:disabled] = true
46
- elsif attr_name == "type"
47
- # button is the default type, so we don't need to do anything.
48
- next if attribute.value == "button"
38
+ if attr_name == "disabled"
39
+ { disabled: true }
40
+ elsif attr_name == "type"
41
+ # button is the default type, so we don't need to do anything.
42
+ return {} if attribute.value == "button"
49
43
 
50
- raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
44
+ raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
51
45
 
52
- args[:type] = ":#{attribute.value}"
53
- else
54
- # Assume the attribute is a system argument.
55
- args.merge!(SystemArguments.new(attribute).to_args)
56
- end
46
+ { type: ":#{attribute.value}" }
57
47
  end
58
-
59
- args
60
48
  end
61
49
 
62
50
  def classes_to_args(classes)
63
- classes.value.split(" ").each_with_object({}) do |class_name, acc|
51
+ classes.split(" ").each_with_object({}) do |class_name, acc|
64
52
  next if class_name == "btn"
65
53
 
66
54
  if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ module ArgumentMappers
8
+ # Maps attributes in the clipboard-copy element to arguments for the ClipboardCopy component.
9
+ class ClipboardCopy < Base
10
+ DEFAULT_TAG = "clipboard-copy"
11
+ ATTRIBUTES = %w[value].freeze
12
+
13
+ def attribute_to_args(attribute)
14
+ Helpers::ErbBlock.raise_if_erb_block(attribute)
15
+ { value: attribute.value.to_json }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
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
+ 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)
14
+ end
15
+
16
+ def any?(attribute)
17
+ attribute.value_node&.children&.any? { |n| n.try(:type) == :erb }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
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 label element to arguments for the Label component.
9
+ class Label < Base
10
+ SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
11
+ component: "Primer::LabelComponent",
12
+ constant: "SCHEME_MAPPINGS",
13
+ symbolize: true
14
+ ).freeze
15
+
16
+ VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
17
+ component: "Primer::LabelComponent",
18
+ constant: "VARIANT_MAPPINGS",
19
+ symbolize: true
20
+ ).freeze
21
+
22
+ DEFAULT_TAG = Primer::ViewComponents::Constants.get(
23
+ component: "Primer::LabelComponent",
24
+ constant: "DEFAULT_TAG"
25
+ ).freeze
26
+
27
+ ATTRIBUTES = %w[title].freeze
28
+
29
+ def attribute_to_args(attribute)
30
+ Helpers::ErbBlock.raise_if_erb_block(attribute)
31
+ { title: attribute.value.to_json }
32
+ end
33
+
34
+ def classes_to_args(classes)
35
+ classes.split(" ").each_with_object({}) do |class_name, acc|
36
+ next if class_name == "Label"
37
+
38
+ if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
39
+ acc[:scheme] = SCHEME_MAPPINGS[class_name]
40
+ elsif VARIANT_MAPPINGS[class_name] && acc[:variant].nil?
41
+ acc[:variant] = VARIANT_MAPPINGS[class_name]
42
+ else
43
+ raise ConversionError, "Cannot convert class \"#{class_name}\""
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ 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
@@ -28,9 +29,11 @@ module ERBLint
28
29
 
29
30
  { test_selector: m[:selector].tr("'", '"') }
30
31
  elsif attr_name == "data-test-selector"
32
+ Helpers::ErbBlock.raise_if_erb_block(attribute)
33
+
31
34
  { test_selector: attribute.value.to_json }
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 }
36
+ Helpers::ErbBlock.raise_if_erb_block(attribute)
34
37
 
35
38
  { "\"#{attr_name}\"" => attribute.value.to_json }
36
39
  else
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argument_mappers/conversion_error"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ # Helper methods for autocorrectable ERB linters.
8
+ module Autocorrectable
9
+ def map_arguments(tag)
10
+ self.class::ARGUMENT_MAPPER.new(tag).to_s
11
+ rescue ArgumentMappers::ConversionError
12
+ nil
13
+ end
14
+
15
+ def correction(args)
16
+ return nil if args.nil?
17
+
18
+ correction = "<%= render #{self.class::COMPONENT}.new"
19
+ correction += "(#{args})" if args.present?
20
+ "#{correction} do %>"
21
+ end
22
+
23
+ def message(args)
24
+ return self.class::MESSAGE if args.nil?
25
+
26
+ "#{self.class::MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "helpers"
4
+ require_relative "autocorrectable"
4
5
  require_relative "argument_mappers/button"
5
6
 
6
7
  module ERBLint
@@ -8,32 +9,17 @@ module ERBLint
8
9
  # Counts the number of times a HTML button is used instead of the component.
9
10
  class ButtonComponentMigrationCounter < Linter
10
11
  include Helpers
12
+ include Autocorrectable
13
+
14
+ TAGS = Primer::ViewComponents::Constants.get(
15
+ component: "Primer::BaseButton",
16
+ constant: "TAG_OPTIONS"
17
+ ).freeze
11
18
 
12
- TAGS = %w[button summary a].freeze
13
19
  CLASSES = %w[btn btn-link].freeze
14
20
  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."
15
-
16
- private
17
-
18
- def map_arguments(tag)
19
- ArgumentMappers::Button.new(tag).to_s
20
- rescue ArgumentMappers::ConversionError
21
- nil
22
- end
23
-
24
- def correction(args)
25
- return nil if args.nil?
26
-
27
- correction = "<%= render Primer::ButtonComponent.new"
28
- correction += "(#{args})" if args.present?
29
- "#{correction} do %>"
30
- end
31
-
32
- def message(args)
33
- return MESSAGE if args.nil?
34
-
35
- "#{MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
36
- end
21
+ ARGUMENT_MAPPER = ArgumentMappers::Button
22
+ COMPONENT = "Primer::ButtonComponent"
37
23
  end
38
24
  end
39
25
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers"
4
+ require_relative "autocorrectable"
5
+ require_relative "argument_mappers/clipboard_copy"
6
+
7
+ module ERBLint
8
+ module Linters
9
+ # Counts the number of times a HTML clipboard-copy is used instead of the component.
10
+ class ClipboardCopyComponentMigrationCounter < Linter
11
+ include Helpers
12
+ include Autocorrectable
13
+
14
+ TAGS = %w[clipboard-copy].freeze
15
+ CLASSES = %w[].freeze
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
+ ARGUMENT_MAPPER = ArgumentMappers::ClipboardCopy
18
+ COMPONENT = "Primer::ClipboardCopy"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ # Counts the number of times a HTML clipboard-copy is used instead of the component.
8
+ class CloseButtonComponentMigrationCounter < Linter
9
+ include Helpers
10
+
11
+ TAGS = %w[button].freeze
12
+ CLASSES = %w[close-button].freeze
13
+ MESSAGE = "We are migrating close-button to use [Primer::CloseButton](https://primer.style/view-components/components/closebutton), please try to use that instead of raw HTML."
14
+ end
15
+ end
16
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "openssl"
5
+ require "primer/view_components/constants"
5
6
 
6
7
  module ERBLint
7
8
  module Linters
@@ -13,58 +14,64 @@ module ERBLint
13
14
  link menuitem meta param source track wbr img
14
15
  ].freeze
15
16
 
17
+ DUMP_FILE = ".erblint-counter-ignore.json"
18
+
16
19
  def self.included(base)
17
20
  base.include(ERBLint::LinterRegistry)
21
+ end
18
22
 
19
- define_method "run" do |processed_source|
20
- @offenses_not_corrected = 0
21
- tags = tags(processed_source)
22
- tag_tree = build_tag_tree(tags)
23
-
24
- tags.each do |tag|
25
- next if tag.closing?
26
- next unless self.class::TAGS&.include?(tag.name)
23
+ def run(processed_source)
24
+ @total_offenses = 0
25
+ @offenses_not_corrected = 0
26
+ tags = tags(processed_source)
27
+ tag_tree = build_tag_tree(tags)
27
28
 
28
- classes = tag.attributes["class"]&.value&.split(" ") || []
29
+ tags.each do |tag|
30
+ next if tag.closing?
31
+ next unless self.class::TAGS&.include?(tag.name)
29
32
 
30
- tag_tree[tag][:offense] = false
33
+ classes = tag.attributes["class"]&.value&.split(" ") || []
31
34
 
32
- next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
35
+ tag_tree[tag][:offense] = false
33
36
 
34
- args = map_arguments(tag)
35
- correction = correction(args)
37
+ next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
36
38
 
37
- tag_tree[tag][:offense] = true
38
- tag_tree[tag][:correctable] = !correction.nil?
39
- tag_tree[tag][:message] = message(args)
40
- tag_tree[tag][:correction] = correction
41
- end
39
+ args = map_arguments(tag)
40
+ correction = correction(args)
42
41
 
43
- tag_tree.each do |tag, h|
44
- next unless h[:offense]
42
+ tag_tree[tag][:offense] = true
43
+ tag_tree[tag][:correctable] = !correction.nil?
44
+ tag_tree[tag][:message] = message(args)
45
+ tag_tree[tag][:correction] = correction
46
+ end
45
47
 
46
- # We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
47
- if h[:correctable]
48
- add_offense(tag.loc, h[:message], h[:correction])
49
- add_offense(h[:closing].loc, h[:message], "<% end %>")
50
- else
51
- @offenses_not_corrected += 1
52
- generate_offense(self.class, processed_source, tag, h[:message])
53
- end
48
+ tag_tree.each do |tag, h|
49
+ next unless h[:offense]
50
+
51
+ @total_offenses += 1
52
+ # We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
53
+ if h[:correctable]
54
+ add_offense(tag.loc, h[:message], h[:correction])
55
+ add_offense(h[:closing].loc, h[:message], "<% end %>")
56
+ else
57
+ @offenses_not_corrected += 1
58
+ generate_offense(self.class, processed_source, tag, h[:message])
54
59
  end
55
-
56
- counter_correct?(processed_source)
57
60
  end
58
61
 
59
- define_method "autocorrect" do |processed_source, offense|
60
- return unless offense.context
62
+ counter_correct?(processed_source)
61
63
 
62
- lambda do |corrector|
63
- if offense.context.include?(counter_disable)
64
- correct_counter(corrector, processed_source, offense)
65
- else
66
- corrector.replace(offense.source_range, offense.context)
67
- end
64
+ dump_data(processed_source) if ENV["DUMP_LINT_DATA"] == "1"
65
+ end
66
+
67
+ def autocorrect(processed_source, offense)
68
+ return unless offense.context
69
+
70
+ lambda do |corrector|
71
+ if offense.context.include?(counter_disable)
72
+ correct_counter(corrector, processed_source, offense)
73
+ else
74
+ corrector.replace(offense.source_range, offense.context)
68
75
  end
69
76
  end
70
77
  end
@@ -186,6 +193,17 @@ module ERBLint
186
193
  offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
187
194
  add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
188
195
  end
196
+
197
+ def dump_data(processed_source)
198
+ return if @total_offenses.zero?
199
+
200
+ data = File.exist?(DUMP_FILE) ? JSON.parse(File.read(DUMP_FILE)) : {}
201
+
202
+ data[processed_source.filename] ||= {}
203
+ data[processed_source.filename][self.class.name.demodulize] = @total_offenses
204
+
205
+ File.write(DUMP_FILE, JSON.pretty_generate(data))
206
+ end
189
207
  end
190
208
  end
191
209
  end