ariadne_view_components 0.0.1

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 (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