primer_view_components 0.0.48 → 0.0.52

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/app/components/primer/base_component.rb +2 -2
  4. data/app/components/primer/beta/avatar.rb +1 -1
  5. data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
  6. data/app/components/primer/beta/avatar_stack.rb +92 -0
  7. data/app/components/primer/beta/truncate.html.erb +5 -0
  8. data/app/components/primer/beta/truncate.rb +110 -0
  9. data/app/components/primer/border_box_component.rb +27 -1
  10. data/app/components/primer/clipboard_copy.html.erb +2 -2
  11. data/app/components/primer/clipboard_copy.rb +1 -1
  12. data/app/components/primer/dropdown.rb +7 -7
  13. data/app/components/primer/icon_button.rb +1 -1
  14. data/app/components/primer/image_crop.html.erb +4 -4
  15. data/app/components/primer/label_component.rb +13 -12
  16. data/app/components/primer/navigation/tab_component.rb +16 -2
  17. data/app/components/primer/progress_bar_component.rb +0 -3
  18. data/app/components/primer/tab_nav_component.rb +4 -3
  19. data/app/components/primer/truncate.rb +1 -1
  20. data/app/components/primer/underline_nav_component.rb +3 -2
  21. data/app/lib/primer/fetch_or_fallback_helper.rb +2 -0
  22. data/app/lib/primer/octicon/cache.rb +1 -1
  23. data/app/lib/primer/tabbed_component_helper.rb +1 -1
  24. data/app/lib/primer/view_helper.rb +1 -0
  25. data/lib/primer/classify.rb +4 -16
  26. data/lib/primer/classify/cache.rb +0 -5
  27. data/lib/primer/classify/flex.rb +1 -1
  28. data/lib/primer/classify/functional_colors.rb +1 -1
  29. data/lib/primer/classify/utilities.rb +51 -13
  30. data/lib/primer/classify/utilities.yml +16 -0
  31. data/lib/primer/classify/validation.rb +18 -0
  32. data/lib/primer/view_components.rb +34 -6
  33. data/lib/primer/view_components/constants.rb +55 -0
  34. data/lib/primer/view_components/linters/argument_mappers/base.rb +100 -0
  35. data/lib/primer/view_components/linters/argument_mappers/button.rb +33 -46
  36. data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +19 -0
  37. data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +67 -0
  38. data/lib/primer/view_components/linters/argument_mappers/label.rb +49 -0
  39. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +6 -5
  40. data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
  41. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
  42. data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
  43. data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
  44. data/lib/primer/view_components/linters/helpers.rb +47 -42
  45. data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
  46. data/lib/primer/view_components/version.rb +1 -1
  47. data/lib/rubocop/config/default.yml +5 -0
  48. data/lib/rubocop/cop/primer.rb +1 -2
  49. data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
  50. data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
  51. data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
  52. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +12 -16
  53. data/lib/tasks/constants.rake +12 -0
  54. data/lib/tasks/coverage.rake +4 -0
  55. data/lib/tasks/docs.rake +27 -25
  56. data/lib/tasks/utilities.rake +9 -13
  57. data/lib/yard/docs_helper.rb +15 -5
  58. data/static/arguments.yml +980 -0
  59. data/static/assets/view-components.svg +18 -0
  60. data/static/classes.yml +182 -0
  61. data/static/constants.json +640 -0
  62. data/static/statuses.json +4 -2
  63. metadata +29 -10
  64. data/app/components/primer/avatar_stack_component.rb +0 -90
@@ -85,6 +85,22 @@
85
85
  - float-md-none
86
86
  - float-lg-none
87
87
  - float-xl-none
88
+ :w:
89
+ :fit:
90
+ - width-fit
91
+ :full:
92
+ - width-full
93
+ :auto:
94
+ - width-auto
95
+ - width-sm-auto
96
+ - width-md-auto
97
+ - width-lg-auto
98
+ - width-xl-auto
99
+ :h:
100
+ :fit:
101
+ - height-fit
102
+ :full:
103
+ - height-full
88
104
  :m:
89
105
  0:
90
106
  - m-0
@@ -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
@@ -7,22 +7,21 @@ require "primer/view_components/engine"
7
7
  module Primer
8
8
  # :nodoc:
9
9
  module ViewComponents
10
- DEFAULT_STATUSES_PATH = File.expand_path("static")
10
+ DEFAULT_STATIC_PATH = File.expand_path("static")
11
11
  DEFAULT_STATUS_FILE_NAME = "statuses.json"
12
+ DEFAULT_CONSTANTS_FILE_NAME = "constants.json"
12
13
 
13
14
  # generate_statuses returns a hash mapping component name to
14
15
  # the component's status sorted alphabetically by the component name.
15
16
  def self.generate_statuses
16
- statuses = Primer::Component.descendants.each_with_object({}) do |component, mem|
17
+ Primer::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
17
18
  mem[component.to_s] = component.status.to_s
18
19
  end
19
-
20
- statuses.sort_by { |k, _v| k }.to_h
21
20
  end
22
21
 
23
22
  # dump_statuses generates the status hash and then serializes
24
23
  # it as json at the given path
25
- def self.dump_statuses(path: DEFAULT_STATUSES_PATH)
24
+ def self.dump_statuses(path: DEFAULT_STATIC_PATH)
26
25
  require "json"
27
26
 
28
27
  statuses = generate_statuses
@@ -35,8 +34,37 @@ module Primer
35
34
 
36
35
  # read_statuses returns a JSON string matching the output of
37
36
  # generate_statuses
38
- def self.read_statuses(path: DEFAULT_STATUSES_PATH)
37
+ def self.read_statuses(path: DEFAULT_STATIC_PATH)
39
38
  File.read(File.join(path, DEFAULT_STATUS_FILE_NAME))
40
39
  end
40
+
41
+ # generate_constants returns a hash mapping component name to
42
+ # all of its constants.
43
+ def self.generate_constants
44
+ Primer::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
45
+ mem[component.to_s] = component.constants(false).sort.each_with_object({}) do |constant, h|
46
+ h[constant] = component.const_get(constant)
47
+ end
48
+ end
49
+ end
50
+
51
+ # dump_constants generates the constants hash and then serializes
52
+ # it as json at the given path
53
+ def self.dump_constants(path: DEFAULT_STATIC_PATH)
54
+ require "json"
55
+
56
+ constants = generate_constants
57
+
58
+ File.open(File.join(path, DEFAULT_CONSTANTS_FILE_NAME), "w") do |f|
59
+ f.write(JSON.pretty_generate(constants))
60
+ f.write($INPUT_RECORD_SEPARATOR)
61
+ end
62
+ end
63
+
64
+ # read_constants returns a JSON string matching the output of
65
+ # generate_constants
66
+ def self.read_constants(path: DEFAULT_STATIC_PATH)
67
+ File.read(File.join(path, DEFAULT_CONSTANTS_FILE_NAME))
68
+ end
41
69
  end
42
70
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Primer
6
+ module ViewComponents
7
+ # A module for constants that are used in the view components.
8
+ class Constants
9
+ CONSTANTS = JSON.parse(
10
+ File.read(
11
+ File.join(File.dirname(__FILE__), "../../../static/constants.json")
12
+ )
13
+ ).freeze
14
+
15
+ class << self
16
+ def get(component:, constant:, invert: true, symbolize: false)
17
+ values = CONSTANTS.dig(component, constant)
18
+
19
+ case values
20
+ when Hash
21
+ format_hash(values, invert, symbolize)
22
+ when Array
23
+ format_array(values, symbolize)
24
+ else
25
+ values
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def format_hash(values, invert, symbolize)
32
+ val = values.invert if invert
33
+ # remove defaults
34
+ val = val.except("", nil)
35
+
36
+ return val.transform_values { |v| symbolize_value(v) } if symbolize
37
+
38
+ val
39
+ end
40
+
41
+ def format_array(values, symbolize)
42
+ val = values.select(&:present?)
43
+
44
+ return val.map { |v| symbolize_value(v) } if symbolize
45
+
46
+ val
47
+ end
48
+
49
+ def symbolize_value(value)
50
+ ":#{value}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "primer/view_components/constants"
4
+ require "primer/classify/utilities"
5
+ require "primer/classify/validation"
6
+ require_relative "conversion_error"
7
+ require_relative "system_arguments"
8
+ require_relative "helpers/erb_block"
9
+
10
+ module ERBLint
11
+ module Linters
12
+ module ArgumentMappers
13
+ # Provides the base interface to implement an `ArgumentMapper`.
14
+ # Override attribute_to_args in a child class to customize its mapping behavior.
15
+ class Base
16
+ DEFAULT_TAG = nil
17
+ ATTRIBUTES = [].freeze
18
+
19
+ def initialize(tag)
20
+ @tag = tag
21
+ end
22
+
23
+ def to_s
24
+ to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
25
+ end
26
+
27
+ def to_args
28
+ args = {}
29
+
30
+ args[:tag] = ":#{@tag.name}" unless self.class::DEFAULT_TAG.nil? || @tag.name == self.class::DEFAULT_TAG
31
+
32
+ @tag.attributes.each do |attribute|
33
+ attr_name = attribute.name
34
+
35
+ if self.class::ATTRIBUTES.include?(attr_name)
36
+ args.merge!(attribute_to_args(attribute))
37
+ elsif attr_name == "class"
38
+ args.merge!(map_classes(attribute))
39
+ else
40
+ # Assume the attribute is a system argument.
41
+ args.merge!(SystemArguments.new(attribute).to_args)
42
+ end
43
+ end
44
+
45
+ args
46
+ end
47
+
48
+ def attribute_to_args(attribute); end
49
+
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)
54
+ args = classes_to_args(system_arguments[:classes])
55
+
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
74
+ end
75
+
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, ... }
79
+ def classes_to_args(classes)
80
+ { classes: classes&.split(" ") || [] }
81
+ end
82
+
83
+ def system_arguments_to_args(classes)
84
+ system_arguments = Primer::Classify::Utilities.classes_to_hash(classes)
85
+
86
+ # need to transform symbols to strings with leading `:`
87
+ system_arguments.transform_values do |v|
88
+ v.is_a?(Symbol) ? ":#{v}" : v
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def erb_helper
95
+ @erb_helper ||= Helpers::ErbBlock.new
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,66 +1,53 @@
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
+ SCHEME_MAPPINGS = Primer::ViewComponents::Constants.get(
11
+ component: "Primer::ButtonComponent",
12
+ constant: "SCHEME_MAPPINGS",
13
+ symbolize: true
14
+ ).freeze
18
15
 
19
- VARIANT_MAPPINGS = {
20
- "btn-sm" => ":small",
21
- "btn-large" => ":large"
22
- }.freeze
16
+ VARIANT_MAPPINGS = Primer::ViewComponents::Constants.get(
17
+ component: "Primer::ButtonComponent",
18
+ constant: "VARIANT_MAPPINGS",
19
+ symbolize: true
20
+ ).freeze
23
21
 
24
- TYPE_OPTIONS = %w[button reset submit].freeze
22
+ TYPE_OPTIONS = Primer::ViewComponents::Constants.get(
23
+ component: "Primer::BaseButton",
24
+ constant: "TYPE_OPTIONS"
25
+ ).freeze
26
+ DEFAULT_TAG = Primer::ViewComponents::Constants.get(
27
+ component: "Primer::BaseButton",
28
+ constant: "DEFAULT_TAG"
29
+ ).freeze
25
30
 
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 = {}
31
+ ATTRIBUTES = %w[disabled type].freeze
36
32
 
37
- args[:tag] = ":#{@tag.name}" unless @tag.name == "button"
33
+ def attribute_to_args(attribute)
34
+ attr_name = attribute.name
38
35
 
39
- @tag.attributes.each do |attribute|
40
- attr_name = attribute.name
36
+ case attr_name
37
+ when "disabled"
38
+ { disabled: true }
39
+ when "type"
40
+ # button is the default type, so we don't need to do anything.
41
+ return {} if attribute.value == "button"
41
42
 
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"
43
+ raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
49
44
 
50
- raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
51
-
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
45
+ { type: ":#{attribute.value}" }
57
46
  end
58
-
59
- args
60
47
  end
61
48
 
62
49
  def classes_to_args(classes)
63
- classes.value.split(" ").each_with_object({}) do |class_name, acc|
50
+ classes.split.each_with_object({ classes: [] }) do |class_name, acc|
64
51
  next if class_name == "btn"
65
52
 
66
53
  if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
@@ -72,7 +59,7 @@ module ERBLint
72
59
  elsif class_name == "BtnGroup-item"
73
60
  acc[:group_item] = true
74
61
  else
75
- raise ConversionError, "Cannot convert class \"#{class_name}\""
62
+ acc[:classes] << class_name
76
63
  end
77
64
  end
78
65
  end
@@ -0,0 +1,19 @@
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
+ { value: erb_helper.convert(attribute) }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ 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
+ # wrap the result in `""` so it is printed as a string
61
+ "\"#{attribute.value.gsub('<%=', '#{').gsub('%>', '}')}\""
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end