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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/app/components/primer/base_component.rb +2 -2
- data/app/components/primer/beta/avatar.rb +1 -1
- data/app/components/primer/{avatar_stack_component.html.erb → beta/avatar_stack.html.erb} +0 -0
- data/app/components/primer/beta/avatar_stack.rb +92 -0
- data/app/components/primer/beta/truncate.html.erb +5 -0
- data/app/components/primer/beta/truncate.rb +110 -0
- data/app/components/primer/border_box_component.rb +27 -1
- data/app/components/primer/clipboard_copy.html.erb +2 -2
- data/app/components/primer/clipboard_copy.rb +1 -1
- data/app/components/primer/dropdown.rb +7 -7
- data/app/components/primer/icon_button.rb +1 -1
- data/app/components/primer/image_crop.html.erb +4 -4
- data/app/components/primer/label_component.rb +13 -12
- data/app/components/primer/navigation/tab_component.rb +16 -2
- data/app/components/primer/progress_bar_component.rb +0 -3
- data/app/components/primer/tab_nav_component.rb +4 -3
- data/app/components/primer/truncate.rb +1 -1
- data/app/components/primer/underline_nav_component.rb +3 -2
- data/app/lib/primer/fetch_or_fallback_helper.rb +2 -0
- data/app/lib/primer/octicon/cache.rb +1 -1
- data/app/lib/primer/tabbed_component_helper.rb +1 -1
- data/app/lib/primer/view_helper.rb +1 -0
- data/lib/primer/classify.rb +4 -16
- data/lib/primer/classify/cache.rb +0 -5
- data/lib/primer/classify/flex.rb +1 -1
- data/lib/primer/classify/functional_colors.rb +1 -1
- data/lib/primer/classify/utilities.rb +51 -13
- data/lib/primer/classify/utilities.yml +16 -0
- data/lib/primer/classify/validation.rb +18 -0
- data/lib/primer/view_components.rb +34 -6
- data/lib/primer/view_components/constants.rb +55 -0
- data/lib/primer/view_components/linters/argument_mappers/base.rb +100 -0
- data/lib/primer/view_components/linters/argument_mappers/button.rb +33 -46
- data/lib/primer/view_components/linters/argument_mappers/clipboard_copy.rb +19 -0
- data/lib/primer/view_components/linters/argument_mappers/helpers/erb_block.rb +67 -0
- data/lib/primer/view_components/linters/argument_mappers/label.rb +49 -0
- data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +6 -5
- data/lib/primer/view_components/linters/autocorrectable.rb +30 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -23
- data/lib/primer/view_components/linters/clipboard_copy_component_migration_counter.rb +21 -0
- data/lib/primer/view_components/linters/close_button_component_migration_counter.rb +16 -0
- data/lib/primer/view_components/linters/helpers.rb +47 -42
- data/lib/primer/view_components/linters/label_component_migration_counter.rb +25 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/rubocop/config/default.yml +5 -0
- data/lib/rubocop/cop/primer.rb +1 -2
- data/lib/rubocop/cop/primer/deprecated_arguments.rb +173 -0
- data/lib/rubocop/cop/primer/no_tag_memoize.rb +1 -0
- data/lib/rubocop/cop/primer/primer_octicon.rb +178 -0
- data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +12 -16
- data/lib/tasks/constants.rake +12 -0
- data/lib/tasks/coverage.rake +4 -0
- data/lib/tasks/docs.rake +27 -25
- data/lib/tasks/utilities.rake +9 -13
- data/lib/yard/docs_helper.rb +15 -5
- data/static/arguments.yml +980 -0
- data/static/assets/view-components.svg +18 -0
- data/static/classes.yml +182 -0
- data/static/constants.json +640 -0
- data/static/statuses.json +4 -2
- metadata +29 -10
- 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
|
-
|
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
|
-
|
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:
|
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:
|
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 "
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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 =
|
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
|
-
|
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
|
-
|
33
|
+
def attribute_to_args(attribute)
|
34
|
+
attr_name = attribute.name
|
38
35
|
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|