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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/app/assets/javascripts/ariadne_view_components.js +2 -0
- data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
- data/app/assets/stylesheets/application.tailwind.css +3 -0
- data/app/components/ariadne/ariadne.ts +14 -0
- data/app/components/ariadne/base_button.rb +60 -0
- data/app/components/ariadne/base_component.rb +155 -0
- data/app/components/ariadne/button_component.html.erb +4 -0
- data/app/components/ariadne/button_component.rb +158 -0
- data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
- data/app/components/ariadne/clipboard_copy_component.rb +50 -0
- data/app/components/ariadne/clipboard_copy_component.ts +19 -0
- data/app/components/ariadne/component.rb +123 -0
- data/app/components/ariadne/content.rb +12 -0
- data/app/components/ariadne/counter_component.rb +100 -0
- data/app/components/ariadne/flash_component.html.erb +31 -0
- data/app/components/ariadne/flash_component.rb +125 -0
- data/app/components/ariadne/heading_component.rb +49 -0
- data/app/components/ariadne/heroicon_component.html.erb +7 -0
- data/app/components/ariadne/heroicon_component.rb +116 -0
- data/app/components/ariadne/image_component.rb +51 -0
- data/app/components/ariadne/text.rb +25 -0
- data/app/components/ariadne/tooltip_component.rb +105 -0
- data/app/lib/ariadne/audited/dsl.rb +32 -0
- data/app/lib/ariadne/class_name_helper.rb +22 -0
- data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
- data/app/lib/ariadne/icon_helper.rb +47 -0
- data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
- data/app/lib/ariadne/logger_helper.rb +23 -0
- data/app/lib/ariadne/status/dsl.rb +41 -0
- data/app/lib/ariadne/tab_nav_helper.rb +35 -0
- data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
- data/app/lib/ariadne/test_selector_helper.rb +20 -0
- data/app/lib/ariadne/underline_nav_helper.rb +44 -0
- data/app/lib/ariadne/view_helper.rb +22 -0
- data/lib/ariadne/classify/utilities.rb +199 -0
- data/lib/ariadne/classify/utilities.yml +1817 -0
- data/lib/ariadne/classify/validation.rb +18 -0
- data/lib/ariadne/classify.rb +210 -0
- data/lib/ariadne/view_components/constants.rb +53 -0
- data/lib/ariadne/view_components/engine.rb +30 -0
- data/lib/ariadne/view_components/linters.rb +3 -0
- data/lib/ariadne/view_components/statuses.rb +14 -0
- data/lib/ariadne/view_components/version.rb +7 -0
- data/lib/ariadne/view_components.rb +59 -0
- data/lib/rubocop/config/default.yml +14 -0
- data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
- data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
- data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
- data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
- data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
- data/lib/rubocop/cop/ariadne.rb +3 -0
- data/lib/tasks/ariadne_view_components.rake +47 -0
- data/lib/tasks/coverage.rake +19 -0
- data/lib/tasks/custom_utilities.yml +310 -0
- data/lib/tasks/docs.rake +525 -0
- data/lib/tasks/helpers/ast_processor.rb +44 -0
- data/lib/tasks/helpers/ast_traverser.rb +77 -0
- data/lib/tasks/static.rake +15 -0
- data/lib/tasks/tailwind.rake +31 -0
- data/lib/tasks/utilities.rake +121 -0
- data/lib/yard/docs_helper.rb +83 -0
- data/lib/yard/renders_many_handler.rb +19 -0
- data/lib/yard/renders_one_handler.rb +19 -0
- data/static/arguments.yml +251 -0
- data/static/assets/view-components.svg +18 -0
- data/static/audited_at.json +14 -0
- data/static/classes.yml +89 -0
- data/static/constants.json +243 -0
- data/static/statuses.json +14 -0
- data/static/tailwindcss.yml +727 -0
- 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,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,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
|