ariadne_view_components 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +68 -0
  4. data/app/assets/javascripts/ariadne_view_components.js +2 -0
  5. data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
  6. data/app/assets/stylesheets/application.tailwind.css +3 -0
  7. data/app/components/ariadne/ariadne.ts +14 -0
  8. data/app/components/ariadne/base_button.rb +60 -0
  9. data/app/components/ariadne/base_component.rb +155 -0
  10. data/app/components/ariadne/button_component.html.erb +4 -0
  11. data/app/components/ariadne/button_component.rb +158 -0
  12. data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
  13. data/app/components/ariadne/clipboard_copy_component.rb +50 -0
  14. data/app/components/ariadne/clipboard_copy_component.ts +19 -0
  15. data/app/components/ariadne/component.rb +123 -0
  16. data/app/components/ariadne/content.rb +12 -0
  17. data/app/components/ariadne/counter_component.rb +100 -0
  18. data/app/components/ariadne/flash_component.html.erb +31 -0
  19. data/app/components/ariadne/flash_component.rb +125 -0
  20. data/app/components/ariadne/heading_component.rb +49 -0
  21. data/app/components/ariadne/heroicon_component.html.erb +7 -0
  22. data/app/components/ariadne/heroicon_component.rb +116 -0
  23. data/app/components/ariadne/image_component.rb +51 -0
  24. data/app/components/ariadne/text.rb +25 -0
  25. data/app/components/ariadne/tooltip_component.rb +105 -0
  26. data/app/lib/ariadne/audited/dsl.rb +32 -0
  27. data/app/lib/ariadne/class_name_helper.rb +22 -0
  28. data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
  29. data/app/lib/ariadne/icon_helper.rb +47 -0
  30. data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
  31. data/app/lib/ariadne/logger_helper.rb +23 -0
  32. data/app/lib/ariadne/status/dsl.rb +41 -0
  33. data/app/lib/ariadne/tab_nav_helper.rb +35 -0
  34. data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
  35. data/app/lib/ariadne/test_selector_helper.rb +20 -0
  36. data/app/lib/ariadne/underline_nav_helper.rb +44 -0
  37. data/app/lib/ariadne/view_helper.rb +22 -0
  38. data/lib/ariadne/classify/utilities.rb +199 -0
  39. data/lib/ariadne/classify/utilities.yml +1817 -0
  40. data/lib/ariadne/classify/validation.rb +18 -0
  41. data/lib/ariadne/classify.rb +210 -0
  42. data/lib/ariadne/view_components/constants.rb +53 -0
  43. data/lib/ariadne/view_components/engine.rb +30 -0
  44. data/lib/ariadne/view_components/linters.rb +3 -0
  45. data/lib/ariadne/view_components/statuses.rb +14 -0
  46. data/lib/ariadne/view_components/version.rb +7 -0
  47. data/lib/ariadne/view_components.rb +59 -0
  48. data/lib/rubocop/config/default.yml +14 -0
  49. data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
  50. data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
  51. data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
  52. data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
  53. data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
  54. data/lib/rubocop/cop/ariadne.rb +3 -0
  55. data/lib/tasks/ariadne_view_components.rake +47 -0
  56. data/lib/tasks/coverage.rake +19 -0
  57. data/lib/tasks/custom_utilities.yml +310 -0
  58. data/lib/tasks/docs.rake +525 -0
  59. data/lib/tasks/helpers/ast_processor.rb +44 -0
  60. data/lib/tasks/helpers/ast_traverser.rb +77 -0
  61. data/lib/tasks/static.rake +15 -0
  62. data/lib/tasks/tailwind.rake +31 -0
  63. data/lib/tasks/utilities.rake +121 -0
  64. data/lib/yard/docs_helper.rb +83 -0
  65. data/lib/yard/renders_many_handler.rb +19 -0
  66. data/lib/yard/renders_one_handler.rb +19 -0
  67. data/static/arguments.yml +251 -0
  68. data/static/assets/view-components.svg +18 -0
  69. data/static/audited_at.json +14 -0
  70. data/static/classes.yml +89 -0
  71. data/static/constants.json +243 -0
  72. data/static/statuses.json +14 -0
  73. data/static/tailwindcss.yml +727 -0
  74. metadata +193 -0
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utilities"
4
+
5
+ module Ariadne
6
+ class Classify
7
+ # :nodoc:
8
+ class Validation
9
+ INVALID_CLASS_NAME_PREFIXES = /box-shadow-|box_shadow-/
10
+
11
+ class << self
12
+ def invalid?(class_name)
13
+ class_name.start_with?(INVALID_CLASS_NAME_PREFIXES) || Ariadne::Classify::Utilities.supported_selector?(class_name)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "classify/utilities"
4
+ require_relative "classify/validation"
5
+
6
+ module Ariadne
7
+ # :nodoc:
8
+ class Classify
9
+ FLEX_VALUES = [1, :auto].freeze
10
+
11
+ FLEX_WRAP_MAPPINGS = {
12
+ wrap: "flex-wrap",
13
+ nowrap: "flex-nowrap",
14
+ reverse: "flex-wrap-reverse",
15
+ }.freeze
16
+
17
+ FLEX_ALIGN_SELF_VALUES = [:auto, :start, :end, :center, :baseline, :stretch].freeze
18
+
19
+ FLEX_DIRECTION_VALUES = [:column, :column_reverse, :row, :row_reverse].freeze
20
+
21
+ FLEX_JUSTIFY_CONTENT_VALUES = [:flex_start, :flex_end, :center, :space_between, :space_around].freeze
22
+
23
+ FLEX_ALIGN_ITEMS_VALUES = [:flex_start, :flex_end, :center, :baseline, :stretch].freeze
24
+
25
+ LOOKUP = Ariadne::Classify::Utilities::UTILITIES
26
+
27
+ class << self
28
+ # Utility for mapping component configuration into Tailwind CSS class names.
29
+ #
30
+ # args can contain utility keys that mimic the interface used by
31
+ # Ariadne components, as well as the special entries :classes
32
+ # and :style.
33
+ #
34
+ # Returns a hash containing two entries. The :classes entry is a string of
35
+ # Tailwind CSS class names, including any classes given in the :classes entry
36
+ # in args. The :style entry is the value of the given :style entry given in
37
+ # args.
38
+ #
39
+ #
40
+ # Example usage:
41
+ # extract_css_attrs({ mt: 4, py: 2 }) => { classes: "mt-4 py-2", style: nil }
42
+ # extract_css_attrs(classes: "d-flex", mt: 4, py: 2) => { classes: "d-flex mt-4 py-2", style: nil }
43
+ # extract_css_attrs(classes: "d-flex", style: "float: left", mt: 4, py: 2) => { classes: "d-flex mt-4 py-2", style: "float: left" }
44
+ #
45
+ def call(args)
46
+ style = nil
47
+ args = [] if args.blank?
48
+
49
+ classes = [].tap do |result|
50
+ args.each do |key, val|
51
+ case key
52
+ when :classes
53
+ # insert :classes first to avoid huge doc diffs
54
+ if (class_names = validated_class_names(val))
55
+ result.unshift(class_names)
56
+ end
57
+ next
58
+ when :style
59
+ style = val
60
+ next
61
+ end
62
+
63
+ next unless LOOKUP[key]
64
+
65
+ if val.is_a?(Array)
66
+ # A while loop is ~3.5x faster than Array#each.
67
+ brk = 0
68
+ while brk < val.size
69
+ item = val[brk]
70
+
71
+ if item.nil?
72
+ brk += 1
73
+ next
74
+ end
75
+
76
+ # Believe it or not, three calls to Hash#[] and an inline rescue
77
+ # are about 30% faster than Hash#dig. It also ensures validate is
78
+ # only called when necessary, i.e. when the class can't be found
79
+ # in the lookup table.
80
+ # rubocop:disable Style/RescueModifier
81
+ found = (LOOKUP[key][item][brk] rescue nil) || validate(key, item, brk)
82
+ # rubocop:enable Style/RescueModifier
83
+ result << found if found
84
+ brk += 1
85
+ end
86
+ else
87
+ next if val.nil?
88
+
89
+ # rubocop:disable Style/RescueModifier
90
+ found = (LOOKUP[key][val][0] rescue nil) || validate(key, val, 0)
91
+ # rubocop:enable Style/RescueModifier
92
+ result << found if found
93
+ end
94
+ end
95
+ end.join(" ")
96
+
97
+ result = {}
98
+
99
+ result[:class] = classes if classes.present?
100
+ result[:style] = style if style.present?
101
+
102
+ result
103
+ end
104
+
105
+ private def validate(key, val, brk)
106
+ brk_str = Ariadne::Classify::Utilities::BREAKPOINTS[brk]
107
+ Ariadne::Classify::Utilities.validate(key, val, brk_str)
108
+ end
109
+
110
+ private def validated_class_names(classes)
111
+ return if classes.blank?
112
+
113
+ corrected_classes = correct_classes(classes)
114
+
115
+ if raise_on_invalid_options? && !ENV["ARIADNE_WARNINGS_DISABLED"]
116
+ invalid_class_names =
117
+ corrected_classes.each_with_object([]) do |class_name, memo|
118
+ memo << class_name if Ariadne::Classify::Validation.invalid?(class_name)
119
+ end
120
+
121
+ # TODO: implement this
122
+ if invalid_class_names.any?
123
+ # raise ArgumentError, <<~MSG
124
+ # Use Tailwind CSS class names instead of your own #{"name".pluralize(invalid_class_names.length)} #{invalid_class_names.to_sentence}.
125
+ # Set ARIADNE_WARNINGS_DISABLED=1 to disable this warning.
126
+ # MSG
127
+ end
128
+ end
129
+
130
+ corrected_classes.join(" ")
131
+ end
132
+
133
+ # TODO: automate this, ugh. peek at utilities.yml
134
+ BG_PREFIX = /^bg-/.freeze
135
+ BG_PSEUDO_PREFIX = /^\S+:bg-/.freeze
136
+ BORDER_PREFIX = /^border-/.freeze
137
+ BORDER_PSEUDO_PREFIX = /^\S+:border-/.freeze
138
+ TEXT_ASPECT_PREFIX = /^text-\S+-/.freeze
139
+ TEXT_ASPECT_PSEUDO_PREFIX = /^\S+:text-\S+-/.freeze
140
+ TEXT_PREFIX = /^text-/.freeze
141
+ TEXT_PSEUDO_PREFIX = /^\S+:text-/.freeze
142
+
143
+ # TODO: TEST!
144
+ private def correct_classes(classes)
145
+ matched_bg = ""
146
+ matched_bg_pseudo = {}
147
+ matched_border = ""
148
+ matched_border_pseudo = {}
149
+ matched_text_aspect = {}
150
+ matched_text_aspect_pseudo = {}
151
+ matched_text = ""
152
+ matched_text_pseudo = {}
153
+
154
+ classes.split(" ").reverse.each_with_object([]) do |c, memo|
155
+ next if c.blank?
156
+
157
+ class_name = c.strip
158
+
159
+ if class_name.match(BG_PREFIX)
160
+ next if matched_bg.present?
161
+
162
+ memo << matched_bg = class_name
163
+ elsif class_name.match(BG_PSEUDO_PREFIX)
164
+ next if matched_bg_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) }
165
+
166
+ matched_bg_pseudo[class_name] = true
167
+ memo << class_name
168
+
169
+ elsif class_name.match(BORDER_PREFIX)
170
+ next if matched_border.present?
171
+
172
+ memo << matched_border = class_name
173
+ elsif class_name.match(BORDER_PSEUDO_PREFIX)
174
+ next if matched_border_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) }
175
+
176
+ matched_border_pseudo[class_name] = true
177
+ memo << class_name
178
+
179
+ elsif class_name.match(TEXT_ASPECT_PREFIX)
180
+ next if matched_text_aspect.keys.any? { |m| m.start_with?(class_name.split(":").first) }
181
+
182
+ matched_text_aspect[class_name] = true
183
+ memo << class_name
184
+ elsif class_name.match(TEXT_ASPECT_PSEUDO_PREFIX)
185
+ next if matched_text_aspect_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) }
186
+
187
+ matched_text_aspect_pseudo[class_name] = true
188
+ memo << class_name
189
+
190
+ elsif class_name.match(TEXT_PREFIX)
191
+ next if matched_text.present?
192
+
193
+ memo << matched_text = class_name
194
+ elsif class_name.match(TEXT_PSEUDO_PREFIX)
195
+ next if matched_text_pseudo.keys.any? { |m| m.start_with?(class_name.split(":").first) }
196
+
197
+ matched_text_pseudo[class_name] = true
198
+ memo << class_name
199
+ else
200
+ memo << class_name
201
+ end
202
+ end.uniq
203
+ end
204
+
205
+ private def raise_on_invalid_options?
206
+ Rails.application.config.ariadne_view_components.raise_on_invalid_options
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ariadne
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 def format_hash(values, invert, symbolize)
30
+ val = invert ? values.invert : values
31
+ # remove defaults
32
+ val = val.except("", nil)
33
+
34
+ return val.transform_values { |v| symbolize_value(v) } if symbolize
35
+
36
+ val
37
+ end
38
+
39
+ private def format_array(values, symbolize)
40
+ val = values.select(&:present?)
41
+
42
+ return val.map { |v| symbolize_value(v) } if symbolize
43
+
44
+ val
45
+ end
46
+
47
+ private def symbolize_value(value)
48
+ ":#{value}"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+ require "ariadne/classify/utilities"
5
+
6
+ module Ariadne
7
+ module ViewComponents
8
+ # :nodoc:
9
+ class Engine < ::Rails::Engine
10
+ isolate_namespace Ariadne::ViewComponents
11
+ config.eager_load_paths = ["#{root}/app/components", "#{root}/app/lib"]
12
+
13
+ config.ariadne_view_components = ActiveSupport::OrderedOptions.new
14
+
15
+ config.ariadne_view_components.raise_on_invalid_options = true
16
+ config.ariadne_view_components.silence_deprecations = false
17
+ config.ariadne_view_components.silence_warnings = false
18
+ config.ariadne_view_components.validate_class_names = true
19
+ config.ariadne_view_components.raise_on_invalid_aria = true
20
+
21
+ initializer "ariadne_view_components.assets" do |app|
22
+ app.config.assets.precompile += ["ariadne_view_components"] if app.config.respond_to?(:assets)
23
+ end
24
+
25
+ config.after_initialize do |app|
26
+ ::Ariadne::Classify::Utilities.validate_class_names = app.config.ariadne_view_components.delete(:validate_class_names)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "linters", "*.rb")].sort.each { |file| require file }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ariadne
6
+ # :nodoc:
7
+ module ViewComponents
8
+ STATUSES = JSON.parse(
9
+ File.read(
10
+ File.join(File.dirname(__FILE__), "../../../static/statuses.json")
11
+ )
12
+ ).freeze
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ module ViewComponents
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ariadne/classify"
4
+ require "ariadne/view_components/version"
5
+ require "ariadne/view_components/engine"
6
+ require "ariadne/view_components/constants"
7
+
8
+ module Ariadne
9
+ # :nodoc:
10
+ module ViewComponents
11
+ DEFAULT_STATIC_PATH = File.expand_path("static")
12
+ FILE_NAMES = {
13
+ statuses: "statuses.json",
14
+ constants: "constants.json",
15
+ audited_at: "audited_at.json",
16
+ }.freeze
17
+
18
+ # generate_statuses returns a hash mapping component name to
19
+ # the component's status sorted alphabetically by the component name.
20
+ def self.generate_statuses
21
+ Ariadne::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
22
+ mem[component.to_s] = component.status.to_s
23
+ end
24
+ end
25
+
26
+ # generate_audited_at returns a hash mapping component name to
27
+ # the day the component has passed an accessibility audit.
28
+ def self.generate_audited_at
29
+ Ariadne::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
30
+ mem[component.to_s] = component.audited_at.to_s
31
+ end
32
+ end
33
+
34
+ # generate_constants returns a hash mapping component name to
35
+ # all of its constants.
36
+ def self.generate_constants
37
+ Ariadne::Component.descendants.sort_by(&:name).each_with_object({}) do |component, mem|
38
+ mem[component.to_s] = component.constants(false).sort.each_with_object({}) do |constant, h|
39
+ h[constant] = component.const_get(constant)
40
+ end
41
+ end
42
+ end
43
+
44
+ # dump generates the requested stat hash and outputs it to a file.
45
+ def self.dump(stats)
46
+ require "json"
47
+
48
+ File.open(File.join(DEFAULT_STATIC_PATH, FILE_NAMES[stats]), "w") do |f|
49
+ f.write(JSON.pretty_generate(send("generate_#{stats}")))
50
+ f.write($INPUT_RECORD_SEPARATOR)
51
+ end
52
+ end
53
+
54
+ # read returns a JSON string matching the output of the corresponding stat.
55
+ def self.read(stats)
56
+ File.read(File.join(DEFAULT_STATIC_PATH, FILE_NAMES[stats]))
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ require:
2
+ - rubocop/cop/ariadne
3
+
4
+ AllCops:
5
+ DisabledByDefault: true
6
+
7
+ Ariadne/SystemArgumentInsteadOfClass:
8
+ Enabled: true
9
+
10
+ Ariadne/NoTagMemoize:
11
+ Enabled: false
12
+
13
+ Ariadne/AriadneHeroicon:
14
+ Enabled: true
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "ariadne/classify/utilities"
5
+ require "ariadne/classify/validation"
6
+
7
+ # :nocov:
8
+ module RuboCop
9
+ module Cop
10
+ module Ariadne
11
+ # This cop ensures that components use System Arguments instead of CSS classes.
12
+ #
13
+ # bad
14
+ # heroicon(icon: :icon, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
15
+ # heroicon(icon: "icon", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
16
+ # heroicon(icon: "icon-with-dashes")
17
+ # heroicon(icon: @ivar)
18
+ # heroicon(icon: condition > "icon" : "other-icon")
19
+ #
20
+ # good
21
+ # ariadne_heroicon(icon: :icon, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
22
+ # ariadne_heroicon(icon: :"icon-with-dashes", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
23
+ # ariadne_heroicon(icon: @ivar, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
24
+ # ariadne_heroicon(icon: condition > "icon" : "other-icon", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)
25
+ class AriadneHeroicon < RuboCop::Cop::Cop
26
+ INVALID_MESSAGE = <<~STR
27
+ Replace the `heroicon` helper with `ariadne_heroicon`. See https://ariadne.style/view-components/components/heroicon for details.
28
+ STR
29
+
30
+ ICON_ATTRIBUTES = ["icon", "variant"].freeze
31
+ SIZE_ATTRIBUTES = ["height", "width", "size"].freeze
32
+ STRING_ATTRIBUTES = ["aria-", "data-"].freeze
33
+ REST_ATTRIBUTES = ["title"].freeze
34
+ VALID_ATTRIBUTES = [*ICON_ATTRIBUTES, *SIZE_ATTRIBUTES, *STRING_ATTRIBUTES, *REST_ATTRIBUTES, "class"].freeze
35
+
36
+ STRING_ATTRIBUTE_REGEX = Regexp.union(STRING_ATTRIBUTES).freeze
37
+ ATTRIBUTE_REGEX = Regexp.union(VALID_ATTRIBUTES).freeze
38
+ INVALID_ATTRIBUTE = -1
39
+
40
+ def on_send(node)
41
+ return unless node.method_name == :heroicon
42
+ return unless node.arguments?
43
+
44
+ kwargs = kwargs(node)
45
+
46
+ return unless kwargs.type == :hash
47
+
48
+ attributes = kwargs.keys.map(&:value)
49
+
50
+ # Don't convert unknown attributes
51
+ return unless attributes.all? { |attribute| attribute.match?(ATTRIBUTE_REGEX) }
52
+
53
+ # Can't convert size
54
+ return if heroicon_size_attributes(kwargs) == INVALID_ATTRIBUTE
55
+
56
+ # find class pair
57
+ classes = classes(kwargs)
58
+
59
+ return if classes == INVALID_ATTRIBUTE
60
+
61
+ # check if classes are convertible
62
+ if classes.present?
63
+ attributes = ::Ariadne::Classify::Utilities.classes_to_hash(classes)
64
+ invalid_classes = (attributes[:classes]&.split(" ") || []).select { |class_name| ::Ariadne::Classify::Validation.invalid?(class_name) }
65
+
66
+ # Uses system argument that can't be converted
67
+ return if invalid_classes.present?
68
+ end
69
+
70
+ add_offense(node, message: INVALID_MESSAGE)
71
+ end
72
+
73
+ def autocorrect(node)
74
+ lambda do |corrector|
75
+ kwargs = kwargs(node)
76
+
77
+ # Converting arguments for the component
78
+ classes = classes(kwargs)
79
+ icon_and_variant = transform_icon_and_variant(kwargs)
80
+ size_attributes = transform_sizes(kwargs)
81
+ rest_attributes = rest_args(kwargs)
82
+
83
+ args = arguments_as_string(node, icon_and_variant, size_attributes, rest_attributes, classes)
84
+
85
+ if node.dot?
86
+ corrector.replace(node.loc.expression, "#{node.receiver.source}.ariadne_heroicon(#{args})")
87
+ else
88
+ corrector.replace(node.loc.expression, "ariadne_heroicon(#{args})")
89
+ end
90
+ end
91
+ end
92
+
93
+ private def transform_icon_and_variant(kwargs)
94
+ kwargs.pairs.each_with_object({}) do |pair, h|
95
+ next unless ICON_ATTRIBUTES.include?(pair.key.value.to_s)
96
+
97
+ # We only support symbol or string values...
98
+ h[pair.key.value] = case pair.value.type
99
+ when :str
100
+ { value: pair.value.value.to_s, type: :str }
101
+ when :sym
102
+ { value: pair.value.source.to_sym, type: :sym }
103
+ else # ... but calling source will also get when you want, for :const, :if, etc.
104
+ { value: pair.value.source, type: :other }
105
+ end
106
+ end
107
+ end
108
+
109
+ private def transform_sizes(kwargs)
110
+ attributes = heroicon_size_attributes(kwargs)
111
+
112
+ attributes.transform_values do |size|
113
+ if size.between?(10, 16)
114
+ ""
115
+ elsif size.between?(22, 26)
116
+ ":medium"
117
+ else
118
+ size
119
+ end
120
+ end
121
+ end
122
+
123
+ private def rest_args(kwargs)
124
+ kwargs.pairs.each_with_object({}) do |pair, h|
125
+ next unless REST_ATTRIBUTES.include?(pair.key.value.to_s)
126
+
127
+ h[pair.key.value] = pair.value.source
128
+ end
129
+ end
130
+
131
+ private def heroicon_size_attributes(kwargs)
132
+ kwargs.pairs.each_with_object({}) do |pair, h|
133
+ next unless SIZE_ATTRIBUTES.include?(pair.key.value.to_s)
134
+
135
+ # We only support string or int values.
136
+ case pair.value.type
137
+ when :int
138
+ h[pair.key.value] = pair.value.source.to_i
139
+ when :str
140
+ h[pair.key.value] = pair.value.value.to_i
141
+ else
142
+ return INVALID_ATTRIBUTE
143
+ end
144
+ end
145
+ end
146
+
147
+ private def classes(kwargs)
148
+ # find class pair
149
+ class_arg = kwargs.pairs.find { |kwarg| kwarg.key.value == :class }
150
+
151
+ return if class_arg.blank?
152
+ return INVALID_ATTRIBUTE unless class_arg.value.type == :str
153
+
154
+ class_arg.value.value
155
+ end
156
+
157
+ private def arguments_as_string(node, icon_and_variant, size_attributes, rest_attributes, classes)
158
+ icon = case icon_and_variant[:icon][:type]
159
+ when :str
160
+ "icon: \"#{icon_and_variant[:icon][:value]}\""
161
+ when :sym, :other
162
+ "icon: #{icon_and_variant[:icon][:value]}"
163
+ end
164
+ variant = case icon_and_variant[:variant][:type]
165
+ when :str
166
+ "variant: \"#{icon_and_variant[:variant][:value]}\""
167
+ when :sym, :other
168
+ "variant: #{icon_and_variant[:variant][:value]}"
169
+ end
170
+
171
+ args = "#{icon}, #{variant}"
172
+
173
+ size_args = size_attributes_to_string(size_attributes)
174
+ string_args = string_args_to_string(node)
175
+ rest_args = rest_args_to_string(rest_attributes)
176
+
177
+ args = "#{args}, #{size_args}" if size_args.present?
178
+ args = "#{args}, #{rest_args}" if rest_args.present?
179
+ args = "#{args}, #{utilities_args(classes)}" if classes.present?
180
+ args = "#{args}, #{string_args}" if string_args.present?
181
+
182
+ args
183
+ end
184
+
185
+ private def rest_args_to_string(attrs)
186
+ return if attrs.blank?
187
+
188
+ attrs.map do |key, value|
189
+ "#{key}: #{value}"
190
+ end.join(", ")
191
+ end
192
+
193
+ private def utilities_args(classes)
194
+ args = ::Ariadne::Classify::Utilities.classes_to_hash(classes)
195
+
196
+ color = case args[:color]
197
+ when :text_white
198
+ :on_emphasis
199
+ when Symbol
200
+ args[:color].to_s.gsub("text_", "icon_").to_sym
201
+ end
202
+
203
+ args[:color] = color if color
204
+
205
+ ::Ariadne::Classify::Utilities.hash_to_args(args)
206
+ end
207
+
208
+ private def size_attributes_to_string(size_attributes)
209
+ # No arguments if they map to the default size
210
+ return if size_attributes.blank? || size_attributes.values.all?(&:blank?)
211
+ # Return mapped argument to `size`
212
+ return "size: :medium" if size_attributes.values.any?(":medium")
213
+
214
+ size_attributes.map do |key, value|
215
+ "#{key}: #{value}"
216
+ end.join(", ")
217
+ end
218
+
219
+ private def string_args_to_string(node)
220
+ kwargs = kwargs(node)
221
+
222
+ args = kwargs.pairs.each_with_object([]) do |pair, acc|
223
+ next unless pair.key.value.to_s.match?(STRING_ATTRIBUTE_REGEX)
224
+
225
+ key = pair.key.value.to_s == "data-test-selector" ? "test_selector" : "\"#{pair.key.value}\""
226
+ acc << "#{key}: #{pair.value.source}"
227
+ end
228
+
229
+ args.join(",")
230
+ end
231
+
232
+ Kwargs = Struct.new(:keys, :pairs, :type)
233
+ def kwargs(node)
234
+ return node.arguments.last if node.arguments.size > 1
235
+
236
+ keys = node.arguments.first.keys
237
+ pairs = node.arguments.first.pairs
238
+ Kwargs.new(keys, pairs, :hash)
239
+ end
240
+
241
+ private def icon(node)
242
+ return node.source unless node.type == :str
243
+ return ":#{node.value}" unless node.value.include?("-")
244
+
245
+ # If the icon contains `-` we need to cast the string as a symbol
246
+ # E.g: `arrow-down` becomes `:"arrow-down"`
247
+ ":#{node.source}"
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "ariadne/view_components/statuses"
5
+ require_relative "../../../../app/lib/ariadne/view_helper"
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Ariadne
10
+ # :nodoc:
11
+ class BaseCop < RuboCop::Cop::Cop
12
+ # We only verify SystemArguments if it's a `.new` call on a component or
13
+ # a ViewHeleper call.
14
+ def valid_node?(node)
15
+ return if node.nil?
16
+
17
+ view_helpers.include?(node.method_name) || (node.method_name == :new && !node.receiver.nil? && ::Ariadne::ViewComponents::STATUSES.key?(node.receiver.const_name))
18
+ end
19
+
20
+ private def view_helpers
21
+ ::Ariadne::ViewHelper::HELPERS.keys.map { |key| "ariadne_#{key}".to_sym }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end