primer_view_components 0.0.43 → 0.0.47
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 +4 -4
- data/CHANGELOG.md +220 -3
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/components/primer/alpha/button_marketing.rb +70 -0
- data/app/components/primer/avatar_stack_component.rb +9 -3
- data/app/components/primer/base_component.rb +52 -23
- data/app/components/primer/beta/auto_complete.rb +159 -0
- data/app/components/primer/beta/auto_complete/auto_complete.d.ts +1 -0
- data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.html.erb +1 -0
- data/app/components/primer/beta/auto_complete/auto_complete.js +1 -0
- data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.ts +0 -0
- data/app/components/primer/beta/auto_complete/item.rb +44 -0
- data/app/components/primer/beta/avatar.rb +77 -0
- data/app/components/primer/beta/text.rb +27 -0
- data/app/components/primer/blankslate_component.rb +2 -1
- data/app/components/primer/border_box_component.rb +3 -0
- data/app/components/primer/button_component.rb +3 -2
- data/app/components/primer/clipboard_copy.rb +25 -7
- data/app/components/primer/component.rb +4 -0
- data/app/components/primer/details_component.rb +18 -3
- data/app/components/primer/dropdown.d.ts +1 -0
- data/app/components/primer/{dropdown_component.html.erb → dropdown.html.erb} +2 -1
- data/app/components/primer/dropdown.js +1 -0
- data/app/components/primer/dropdown.rb +149 -0
- data/app/components/primer/dropdown.ts +1 -0
- data/app/components/primer/dropdown/menu.d.ts +1 -0
- data/app/components/primer/dropdown/menu.html.erb +25 -0
- data/app/components/primer/dropdown/menu.js +1 -0
- data/app/components/primer/dropdown/menu.rb +99 -0
- data/app/components/primer/dropdown/menu.ts +1 -0
- data/app/components/primer/heading_component.rb +1 -1
- data/app/components/primer/icon_button.rb +1 -1
- data/app/components/primer/image_crop.rb +1 -1
- data/app/components/primer/markdown.rb +9 -9
- data/app/components/primer/menu_component.rb +7 -3
- data/app/components/primer/navigation/tab_component.rb +6 -6
- data/app/components/primer/octicon_component.rb +3 -2
- data/app/components/primer/popover_component.rb +6 -3
- data/app/components/primer/primer.d.ts +2 -1
- data/app/components/primer/primer.js +2 -1
- data/app/components/primer/primer.ts +2 -1
- data/app/components/primer/spinner_component.rb +2 -0
- data/app/components/primer/tab_nav_component.rb +5 -3
- data/app/components/primer/timeline_item_component.rb +2 -2
- data/app/components/primer/tooltip.rb +1 -1
- data/app/components/primer/truncate.rb +5 -0
- data/app/components/primer/underline_nav_component.rb +10 -4
- data/{app/lib → lib}/primer/classify.rb +16 -33
- data/{app/lib → lib}/primer/classify/cache.rb +6 -40
- data/{app/lib → lib}/primer/classify/flex.rb +0 -0
- data/{app/lib → lib}/primer/classify/functional_background_colors.rb +2 -0
- data/{app/lib → lib}/primer/classify/functional_border_colors.rb +2 -0
- data/{app/lib → lib}/primer/classify/functional_colors.rb +0 -0
- data/{app/lib → lib}/primer/classify/functional_text_colors.rb +2 -0
- data/{app/lib → lib}/primer/classify/grid.rb +0 -0
- data/lib/primer/classify/utilities.rb +148 -0
- data/lib/primer/classify/utilities.yml +1271 -0
- data/lib/primer/view_components.rb +1 -0
- data/lib/primer/view_components/linters/argument_mappers/button.rb +82 -0
- data/lib/primer/view_components/linters/argument_mappers/conversion_error.rb +10 -0
- data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +47 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +24 -1
- data/lib/primer/view_components/linters/flash_component_migration_counter.rb +1 -1
- data/lib/primer/view_components/linters/helpers.rb +137 -18
- data/lib/primer/view_components/statuses.rb +14 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/tasks/docs.rake +179 -110
- data/lib/tasks/utilities.rake +105 -0
- data/lib/yard/docs_helper.rb +13 -3
- data/static/statuses.json +9 -7
- metadata +41 -27
- data/app/components/primer/auto_complete.rb +0 -100
- data/app/components/primer/auto_complete/item.rb +0 -42
- data/app/components/primer/avatar_component.rb +0 -75
- data/app/components/primer/button_marketing_component.rb +0 -68
- data/app/components/primer/dropdown/menu_component.html.erb +0 -12
- data/app/components/primer/dropdown/menu_component.rb +0 -46
- data/app/components/primer/dropdown_component.rb +0 -73
- data/app/components/primer/text_component.rb +0 -25
- data/app/lib/primer/classify/spacing.rb +0 -63
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "conversion_error"
|
4
|
+
require_relative "system_arguments"
|
5
|
+
|
6
|
+
module ERBLint
|
7
|
+
module Linters
|
8
|
+
module ArgumentMappers
|
9
|
+
# Maps classes in a button element to arguments for the Button component.
|
10
|
+
class Button
|
11
|
+
SCHEME_MAPPINGS = {
|
12
|
+
"btn-primary" => ":primary",
|
13
|
+
"btn-danger" => ":danger",
|
14
|
+
"btn-outline" => ":outline",
|
15
|
+
"btn-invisible" => ":invisible",
|
16
|
+
"btn-link" => ":link"
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
VARIANT_MAPPINGS = {
|
20
|
+
"btn-sm" => ":small",
|
21
|
+
"btn-large" => ":large"
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
TYPE_OPTIONS = %w[button reset submit].freeze
|
25
|
+
|
26
|
+
def initialize(tag)
|
27
|
+
@tag = tag
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
to_args.map { |k, v| "#{k}: #{v}" }.join(", ")
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_args
|
35
|
+
args = {}
|
36
|
+
|
37
|
+
args[:tag] = ":#{@tag.name}" unless @tag.name == "button"
|
38
|
+
|
39
|
+
@tag.attributes.each do |attribute|
|
40
|
+
attr_name = attribute.name
|
41
|
+
|
42
|
+
if attr_name == "class"
|
43
|
+
args = args.merge(classes_to_args(attribute))
|
44
|
+
elsif attr_name == "disabled"
|
45
|
+
args[:disabled] = true
|
46
|
+
elsif attr_name == "type"
|
47
|
+
# button is the default type, so we don't need to do anything.
|
48
|
+
next if attribute.value == "button"
|
49
|
+
|
50
|
+
raise ConversionError, "Button component does not support type \"#{attribute.value}\"" unless TYPE_OPTIONS.include?(attribute.value)
|
51
|
+
|
52
|
+
args[:type] = ":#{attribute.value}"
|
53
|
+
else
|
54
|
+
# Assume the attribute is a system argument.
|
55
|
+
args.merge!(SystemArguments.new(attribute).to_args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
args
|
60
|
+
end
|
61
|
+
|
62
|
+
def classes_to_args(classes)
|
63
|
+
classes.value.split(" ").each_with_object({}) do |class_name, acc|
|
64
|
+
next if class_name == "btn"
|
65
|
+
|
66
|
+
if SCHEME_MAPPINGS[class_name] && acc[:scheme].nil?
|
67
|
+
acc[:scheme] = SCHEME_MAPPINGS[class_name]
|
68
|
+
elsif VARIANT_MAPPINGS[class_name] && acc[:variant].nil?
|
69
|
+
acc[:variant] = VARIANT_MAPPINGS[class_name]
|
70
|
+
elsif class_name == "btn-block"
|
71
|
+
acc[:block] = true
|
72
|
+
elsif class_name == "BtnGroup-item"
|
73
|
+
acc[:group_item] = true
|
74
|
+
else
|
75
|
+
raise ConversionError, "Cannot convert class \"#{class_name}\""
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "conversion_error"
|
4
|
+
|
5
|
+
module ERBLint
|
6
|
+
module Linters
|
7
|
+
module ArgumentMappers
|
8
|
+
# Maps element attributes to system arguments.
|
9
|
+
class SystemArguments
|
10
|
+
STRING_PARAMETERS = %w[aria- data-].freeze
|
11
|
+
TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
|
12
|
+
|
13
|
+
attr_reader :attribute
|
14
|
+
def initialize(attribute)
|
15
|
+
@attribute = attribute
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_args
|
19
|
+
if attribute.erb?
|
20
|
+
_, _, code_node = *attribute.node
|
21
|
+
|
22
|
+
raise ConversionError, "Cannot convert erb block" if code_node.nil?
|
23
|
+
|
24
|
+
code = code_node.loc.source.strip
|
25
|
+
m = code.match(TEST_SELECTOR_REGEX)
|
26
|
+
|
27
|
+
raise ConversionError, "Cannot convert erb block" if m.blank?
|
28
|
+
|
29
|
+
{ test_selector: m[:selector].tr("'", '"') }
|
30
|
+
elsif attr_name == "data-test-selector"
|
31
|
+
{ test_selector: attribute.value.to_json }
|
32
|
+
elsif attr_name.start_with?(*STRING_PARAMETERS)
|
33
|
+
raise ConversionError, "Cannot convert attribute \"#{attr_name}\" because its value contains an erb block" if attribute.value_node&.children&.any? { |n| n.try(:type) == :erb }
|
34
|
+
|
35
|
+
{ "\"#{attr_name}\"" => attribute.value.to_json }
|
36
|
+
else
|
37
|
+
raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def attr_name
|
42
|
+
attribute.name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "helpers"
|
4
|
+
require_relative "argument_mappers/button"
|
4
5
|
|
5
6
|
module ERBLint
|
6
7
|
module Linters
|
@@ -9,8 +10,30 @@ module ERBLint
|
|
9
10
|
include Helpers
|
10
11
|
|
11
12
|
TAGS = %w[button summary a].freeze
|
12
|
-
|
13
|
+
CLASSES = %w[btn btn-link].freeze
|
13
14
|
MESSAGE = "We are migrating buttons to use [Primer::ButtonComponent](https://primer.style/view-components/components/button), please try to use that instead of raw HTML."
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def map_arguments(tag)
|
19
|
+
ArgumentMappers::Button.new(tag).to_s
|
20
|
+
rescue ArgumentMappers::ConversionError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def correction(args)
|
25
|
+
return nil if args.nil?
|
26
|
+
|
27
|
+
correction = "<%= render Primer::ButtonComponent.new"
|
28
|
+
correction += "(#{args})" if args.present?
|
29
|
+
"#{correction} do %>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def message(args)
|
33
|
+
return MESSAGE if args.nil?
|
34
|
+
|
35
|
+
"#{MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
|
36
|
+
end
|
14
37
|
end
|
15
38
|
end
|
16
39
|
end
|
@@ -9,7 +9,7 @@ module ERBLint
|
|
9
9
|
include Helpers
|
10
10
|
|
11
11
|
TAGS = %w[div].freeze
|
12
|
-
|
12
|
+
CLASSES = %w[flash].freeze
|
13
13
|
MESSAGE = "We are migrating flashes to use [Primer::FlashComponent](https://primer.style/view-components/components/flash), please try to use that instead of raw HTML."
|
14
14
|
end
|
15
15
|
end
|
@@ -7,34 +7,69 @@ module ERBLint
|
|
7
7
|
module Linters
|
8
8
|
# Helper methods for linting ERB.
|
9
9
|
module Helpers
|
10
|
+
# from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
|
11
|
+
SELF_CLOSING_TAGS = %w[
|
12
|
+
area base br col command embed hr input keygen
|
13
|
+
link menuitem meta param source track wbr img
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
DUMP_FILE = ".erblint-counter-ignore.json"
|
17
|
+
|
10
18
|
def self.included(base)
|
11
|
-
base.include(LinterRegistry)
|
19
|
+
base.include(ERBLint::LinterRegistry)
|
12
20
|
|
13
21
|
define_method "run" do |processed_source|
|
14
|
-
|
22
|
+
@total_offenses = 0
|
23
|
+
@offenses_not_corrected = 0
|
24
|
+
tags = tags(processed_source)
|
25
|
+
tag_tree = build_tag_tree(tags)
|
26
|
+
|
27
|
+
tags.each do |tag|
|
15
28
|
next if tag.closing?
|
16
29
|
next unless self.class::TAGS&.include?(tag.name)
|
17
30
|
|
18
|
-
classes = tag.attributes["class"]&.value&.split(" ")
|
31
|
+
classes = tag.attributes["class"]&.value&.split(" ") || []
|
19
32
|
|
20
|
-
|
33
|
+
tag_tree[tag][:offense] = false
|
21
34
|
|
22
|
-
|
35
|
+
next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
|
36
|
+
|
37
|
+
args = map_arguments(tag)
|
38
|
+
correction = correction(args)
|
39
|
+
|
40
|
+
tag_tree[tag][:offense] = true
|
41
|
+
tag_tree[tag][:correctable] = !correction.nil?
|
42
|
+
tag_tree[tag][:message] = message(args)
|
43
|
+
tag_tree[tag][:correction] = correction
|
44
|
+
end
|
45
|
+
|
46
|
+
tag_tree.each do |tag, h|
|
47
|
+
next unless h[:offense]
|
48
|
+
|
49
|
+
@total_offenses += 1
|
50
|
+
# We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
|
51
|
+
if h[:correctable]
|
52
|
+
add_offense(tag.loc, h[:message], h[:correction])
|
53
|
+
add_offense(h[:closing].loc, h[:message], "<% end %>")
|
54
|
+
else
|
55
|
+
@offenses_not_corrected += 1
|
56
|
+
generate_offense(self.class, processed_source, tag, h[:message])
|
57
|
+
end
|
23
58
|
end
|
24
59
|
|
25
60
|
counter_correct?(processed_source)
|
61
|
+
|
62
|
+
dump_data(processed_source) if ENV["DUMP_LINT_DATA"] == "1"
|
26
63
|
end
|
27
64
|
|
28
65
|
define_method "autocorrect" do |processed_source, offense|
|
29
66
|
return unless offense.context
|
30
67
|
|
31
68
|
lambda do |corrector|
|
32
|
-
if
|
33
|
-
|
34
|
-
corrector.replace(offense.source_range, offense.context)
|
69
|
+
if offense.context.include?(counter_disable)
|
70
|
+
correct_counter(corrector, processed_source, offense)
|
35
71
|
else
|
36
|
-
|
37
|
-
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
|
72
|
+
corrector.replace(offense.source_range, offense.context)
|
38
73
|
end
|
39
74
|
end
|
40
75
|
end
|
@@ -42,6 +77,77 @@ module ERBLint
|
|
42
77
|
|
43
78
|
private
|
44
79
|
|
80
|
+
# Override this function to convert the HTML element attributes to argument for a component.
|
81
|
+
#
|
82
|
+
# @return [Hash] if possible to map all attributes to arguments.
|
83
|
+
# @return [Nil] if cannot map to arguments.
|
84
|
+
def map_arguments(_tag)
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Override this function to define how to autocorrect an element to a component.
|
89
|
+
#
|
90
|
+
# @return [String] with the text to replace the HTML element if possible to correct.
|
91
|
+
# @return [Nil] if cannot correct element.
|
92
|
+
def correction(_tag)
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Override this function to customize the linter message.
|
97
|
+
#
|
98
|
+
# @return [String] message to show on linter error.
|
99
|
+
def message(_tag)
|
100
|
+
self.class::MESSAGE
|
101
|
+
end
|
102
|
+
|
103
|
+
def counter_disable
|
104
|
+
"erblint:counter #{self.class.name.demodulize}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def correct_counter(corrector, processed_source, offense)
|
108
|
+
if processed_source.file_content.include?(counter_disable)
|
109
|
+
# update the counter if exists
|
110
|
+
corrector.replace(offense.source_range, offense.context)
|
111
|
+
else
|
112
|
+
# add comment with counter if none
|
113
|
+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
|
118
|
+
# From the tags, we build a structured tree which represents the tag hierarchy.
|
119
|
+
# With this, we are able to know where the tags start and end.
|
120
|
+
def build_tag_tree(tags)
|
121
|
+
tag_tree = {}
|
122
|
+
current_opened_tag = nil
|
123
|
+
|
124
|
+
tags.each do |tag|
|
125
|
+
if tag.closing?
|
126
|
+
if current_opened_tag && tag.name == current_opened_tag.name
|
127
|
+
tag_tree[current_opened_tag][:closing] = tag
|
128
|
+
current_opened_tag = tag_tree[current_opened_tag][:parent]
|
129
|
+
end
|
130
|
+
|
131
|
+
next
|
132
|
+
end
|
133
|
+
|
134
|
+
self_closing = self_closing?(tag)
|
135
|
+
|
136
|
+
tag_tree[tag] = {
|
137
|
+
closing: self_closing ? tag : nil,
|
138
|
+
parent: current_opened_tag
|
139
|
+
}
|
140
|
+
|
141
|
+
current_opened_tag = tag unless self_closing
|
142
|
+
end
|
143
|
+
|
144
|
+
tag_tree
|
145
|
+
end
|
146
|
+
|
147
|
+
def self_closing?(tag)
|
148
|
+
tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
|
149
|
+
end
|
150
|
+
|
45
151
|
def tags(processed_source)
|
46
152
|
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
|
47
153
|
end
|
@@ -50,7 +156,6 @@ module ERBLint
|
|
50
156
|
comment_node = nil
|
51
157
|
expected_count = 0
|
52
158
|
rule_name = self.class.name.match(/:?:?(\w+)\Z/)[1]
|
53
|
-
offenses_count = @offenses.length
|
54
159
|
|
55
160
|
processed_source.parser.ast.descendants(:erb).each do |node|
|
56
161
|
indicator_node, _, code_node, = *node
|
@@ -58,32 +163,46 @@ module ERBLint
|
|
58
163
|
comment = code_node&.loc&.source&.strip
|
59
164
|
|
60
165
|
if indicator == "#" && comment.start_with?("erblint:count") && comment.match(rule_name)
|
61
|
-
comment_node =
|
166
|
+
comment_node = node
|
62
167
|
expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
|
63
168
|
end
|
64
169
|
end
|
65
170
|
|
66
|
-
if
|
67
|
-
|
171
|
+
if @offenses_not_corrected.zero?
|
172
|
+
# have to adjust to get `\n` so we delete the whole line
|
173
|
+
add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:count comment for #{rule_name}", "") if comment_node
|
68
174
|
return
|
69
175
|
end
|
70
176
|
|
71
177
|
first_offense = @offenses[0]
|
72
178
|
|
73
179
|
if comment_node.nil?
|
74
|
-
add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{
|
75
|
-
|
180
|
+
add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{@offenses_not_corrected} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
|
181
|
+
elsif expected_count != @offenses_not_corrected
|
182
|
+
add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{@offenses_not_corrected}.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
|
183
|
+
# the only offenses remaining are not autocorrectable, so we can ignore them
|
184
|
+
elsif expected_count == @offenses_not_corrected && @offenses.size == @offenses_not_corrected
|
76
185
|
clear_offenses
|
77
|
-
add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{offenses_count}.", " erblint:counter #{rule_name} #{offenses_count} ") if expected_count != offenses_count
|
78
186
|
end
|
79
187
|
end
|
80
188
|
|
81
189
|
def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
|
82
190
|
message ||= klass::MESSAGE
|
83
|
-
klass_name = klass.name.
|
191
|
+
klass_name = klass.name.demodulize
|
84
192
|
offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
|
85
193
|
add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
|
86
194
|
end
|
195
|
+
|
196
|
+
def dump_data(processed_source)
|
197
|
+
return if @total_offenses.zero?
|
198
|
+
|
199
|
+
data = File.exist?(DUMP_FILE) ? JSON.parse(File.read(DUMP_FILE)) : {}
|
200
|
+
|
201
|
+
data[processed_source.filename] ||= {}
|
202
|
+
data[processed_source.filename][self.class.name.demodulize] = @total_offenses
|
203
|
+
|
204
|
+
File.write(DUMP_FILE, JSON.pretty_generate(data))
|
205
|
+
end
|
87
206
|
end
|
88
207
|
end
|
89
208
|
end
|
data/lib/tasks/docs.rake
CHANGED
@@ -32,9 +32,9 @@ namespace :docs do
|
|
32
32
|
Primer::OcticonSymbolsComponent,
|
33
33
|
Primer::ImageCrop,
|
34
34
|
Primer::IconButton,
|
35
|
-
Primer::AutoComplete,
|
36
|
-
Primer::AutoComplete::Item,
|
37
|
-
Primer::
|
35
|
+
Primer::Beta::AutoComplete,
|
36
|
+
Primer::Beta::AutoComplete::Item,
|
37
|
+
Primer::Beta::Avatar,
|
38
38
|
Primer::AvatarStackComponent,
|
39
39
|
Primer::BaseButton,
|
40
40
|
Primer::BlankslateComponent,
|
@@ -43,12 +43,12 @@ namespace :docs do
|
|
43
43
|
Primer::BreadcrumbComponent,
|
44
44
|
Primer::ButtonComponent,
|
45
45
|
Primer::ButtonGroup,
|
46
|
-
Primer::
|
46
|
+
Primer::Alpha::ButtonMarketing,
|
47
47
|
Primer::ClipboardCopy,
|
48
48
|
Primer::CloseButton,
|
49
49
|
Primer::CounterComponent,
|
50
50
|
Primer::DetailsComponent,
|
51
|
-
Primer::
|
51
|
+
Primer::Dropdown,
|
52
52
|
Primer::DropdownMenuComponent,
|
53
53
|
Primer::FlashComponent,
|
54
54
|
Primer::FlexComponent,
|
@@ -69,7 +69,7 @@ namespace :docs do
|
|
69
69
|
Primer::SubheadComponent,
|
70
70
|
Primer::TabContainerComponent,
|
71
71
|
Primer::TabNavComponent,
|
72
|
-
Primer::
|
72
|
+
Primer::Beta::Text,
|
73
73
|
Primer::TimeAgoComponent,
|
74
74
|
Primer::TimelineItemComponent,
|
75
75
|
Primer::Tooltip,
|
@@ -78,9 +78,10 @@ namespace :docs do
|
|
78
78
|
]
|
79
79
|
|
80
80
|
js_components = [
|
81
|
+
Primer::Dropdown,
|
81
82
|
Primer::LocalTime,
|
82
83
|
Primer::ImageCrop,
|
83
|
-
Primer::AutoComplete,
|
84
|
+
Primer::Beta::AutoComplete,
|
84
85
|
Primer::ClipboardCopy,
|
85
86
|
Primer::TabContainerComponent,
|
86
87
|
Primer::TabNavComponent,
|
@@ -91,30 +92,35 @@ namespace :docs do
|
|
91
92
|
all_components = Primer::Component.descendants - [Primer::BaseComponent]
|
92
93
|
components_needing_docs = all_components - components
|
93
94
|
|
94
|
-
components_without_examples = []
|
95
95
|
args_for_components = []
|
96
96
|
classes_found_in_examples = []
|
97
97
|
|
98
|
-
|
98
|
+
errors = []
|
99
|
+
|
100
|
+
# Deletes docs before regenerating them, guaranteeing that we don't keep stale docs.
|
101
|
+
FileUtils.rm_rf(Dir.glob("docs/content/components/**/*.md"))
|
102
|
+
|
103
|
+
components.sort_by(&:name).each do |component|
|
99
104
|
documentation = registry.get(component.name)
|
100
105
|
|
101
|
-
|
102
|
-
short_name = component.name.gsub(/Primer|::|Component/, "")
|
106
|
+
data = docs_metadata(component)
|
103
107
|
|
104
|
-
path = Pathname.new(
|
108
|
+
path = Pathname.new(data[:path])
|
105
109
|
path.dirname.mkdir unless path.dirname.exist?
|
106
110
|
File.open(path, "w") do |f|
|
107
111
|
f.puts("---")
|
108
|
-
f.puts("title: #{
|
109
|
-
f.puts("status: #{
|
110
|
-
f.puts("source:
|
111
|
-
f.puts("storybook:
|
112
|
+
f.puts("title: #{data[:title]}")
|
113
|
+
f.puts("status: #{data[:status]}")
|
114
|
+
f.puts("source: #{data[:source]}")
|
115
|
+
f.puts("storybook: #{data[:storybook]}")
|
112
116
|
f.puts("---")
|
113
117
|
f.puts
|
114
|
-
f.puts("import Example from '
|
118
|
+
f.puts("import Example from '#{data[:example_path]}'")
|
119
|
+
|
120
|
+
initialize_method = documentation.meths.find(&:constructor?)
|
115
121
|
|
116
122
|
if js_components.include?(component)
|
117
|
-
f.puts("import RequiresJSFlash from '
|
123
|
+
f.puts("import RequiresJSFlash from '#{data[:require_js_path]}'")
|
118
124
|
f.puts
|
119
125
|
f.puts("<RequiresJSFlash />")
|
120
126
|
end
|
@@ -124,108 +130,68 @@ namespace :docs do
|
|
124
130
|
f.puts
|
125
131
|
f.puts(view_context.render(inline: documentation.base_docstring))
|
126
132
|
|
127
|
-
if documentation.tags(:
|
133
|
+
if documentation.tags(:deprecated).any?
|
128
134
|
f.puts
|
129
|
-
f.puts("##
|
130
|
-
documentation.tags(:
|
135
|
+
f.puts("## Deprecation")
|
136
|
+
documentation.tags(:deprecated).each do |tag|
|
131
137
|
f.puts
|
132
138
|
f.puts view_context.render(inline: tag.text)
|
133
139
|
end
|
134
140
|
end
|
135
141
|
|
136
|
-
if documentation.tags(:
|
142
|
+
if documentation.tags(:accessibility).any?
|
137
143
|
f.puts
|
138
|
-
f.puts("##
|
139
|
-
documentation.tags(:
|
144
|
+
f.puts("## Accessibility")
|
145
|
+
documentation.tags(:accessibility).each do |tag|
|
140
146
|
f.puts
|
141
147
|
f.puts view_context.render(inline: tag.text)
|
142
148
|
end
|
143
149
|
end
|
144
150
|
|
145
|
-
|
151
|
+
params = initialize_method.tags(:param)
|
146
152
|
|
147
|
-
|
148
|
-
f.puts
|
149
|
-
f.puts("## Examples")
|
150
|
-
else
|
151
|
-
components_without_examples << component
|
152
|
-
end
|
153
|
+
errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
|
153
154
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
155
|
+
f.puts
|
156
|
+
f.puts("## Arguments")
|
157
|
+
f.puts
|
158
|
+
f.puts("| Name | Type | Default | Description |")
|
159
|
+
f.puts("| :- | :- | :- | :- |")
|
158
160
|
|
159
|
-
|
160
|
-
|
161
|
-
description = splitted.second.gsub(/^[ \t]{2}/, "").strip
|
162
|
-
code = splitted.last.gsub(/^[ \t]{2}/, "").strip
|
163
|
-
else
|
164
|
-
code = tag.text
|
165
|
-
end
|
161
|
+
docummented_params = params.map(&:name)
|
162
|
+
component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
|
166
163
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
f.puts(description)
|
164
|
+
if (docummented_params & component_params).size != component_params.size
|
165
|
+
err = { arguments: {} }
|
166
|
+
(component_params - docummented_params).each do |arg|
|
167
|
+
err[:arguments][arg] = "Not documented"
|
172
168
|
end
|
173
|
-
|
174
|
-
|
175
|
-
html.scan(/class="([^"]*)"/) do |classnames|
|
176
|
-
classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
|
177
|
-
end
|
178
|
-
f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
|
179
|
-
f.puts
|
180
|
-
f.puts("```erb")
|
181
|
-
f.puts(code.to_s)
|
182
|
-
f.puts("```")
|
169
|
+
|
170
|
+
errors << { component.name => err }
|
183
171
|
end
|
184
172
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
f.puts("## Arguments")
|
189
|
-
f.puts
|
190
|
-
f.puts("| Name | Type | Default | Description |")
|
191
|
-
f.puts("| :- | :- | :- | :- |")
|
192
|
-
|
193
|
-
args = []
|
194
|
-
params.each do |tag|
|
195
|
-
params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
|
196
|
-
|
197
|
-
default =
|
198
|
-
if params && params[1]
|
199
|
-
constant_name = "#{component.name}::#{params[1]}"
|
200
|
-
constant_value = constant_name.safe_constantize
|
201
|
-
if constant_value.nil?
|
202
|
-
pretty_value(params[1])
|
203
|
-
else
|
204
|
-
pretty_value(constant_value)
|
205
|
-
end
|
206
|
-
else
|
207
|
-
"N/A"
|
208
|
-
end
|
209
|
-
|
210
|
-
args << {
|
211
|
-
"name" => tag.name,
|
212
|
-
"type" => tag.types.join(", "),
|
213
|
-
"default" => default,
|
214
|
-
"description" => view_context.render(inline: tag.text)
|
215
|
-
}
|
216
|
-
|
217
|
-
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |")
|
218
|
-
end
|
173
|
+
args = []
|
174
|
+
params.each do |tag|
|
175
|
+
default_value = pretty_default_value(tag, component)
|
219
176
|
|
220
|
-
|
221
|
-
"
|
222
|
-
"
|
223
|
-
"
|
177
|
+
args << {
|
178
|
+
"name" => tag.name,
|
179
|
+
"type" => tag.types.join(", "),
|
180
|
+
"default" => default_value,
|
181
|
+
"description" => view_context.render(inline: tag.text.squish)
|
224
182
|
}
|
225
183
|
|
226
|
-
|
184
|
+
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
|
227
185
|
end
|
228
186
|
|
187
|
+
component_args = {
|
188
|
+
"component" => data[:title],
|
189
|
+
"source" => data[:source],
|
190
|
+
"parameters" => args
|
191
|
+
}
|
192
|
+
|
193
|
+
args_for_components << component_args
|
194
|
+
|
229
195
|
# Slots V2 docs
|
230
196
|
slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
|
231
197
|
|
@@ -250,22 +216,61 @@ namespace :docs do
|
|
250
216
|
end
|
251
217
|
|
252
218
|
param_tags.each do |tag|
|
253
|
-
|
219
|
+
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
254
223
|
|
255
|
-
|
256
|
-
if params && params[1]
|
257
|
-
"`#{params[1]}`"
|
258
|
-
else
|
259
|
-
"N/A"
|
260
|
-
end
|
224
|
+
errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
|
261
225
|
|
262
|
-
|
263
|
-
|
226
|
+
f.puts
|
227
|
+
f.puts("## Examples")
|
228
|
+
|
229
|
+
initialize_method.tags(:example).each do |tag|
|
230
|
+
name = tag.name
|
231
|
+
description = nil
|
232
|
+
code = nil
|
233
|
+
|
234
|
+
if tag.text.include?("@description")
|
235
|
+
splitted = tag.text.split(/@description|@code/)
|
236
|
+
description = splitted.second.gsub(/^[ \t]{2}/, "").strip
|
237
|
+
code = splitted.last.gsub(/^[ \t]{2}/, "").strip
|
238
|
+
else
|
239
|
+
code = tag.text
|
240
|
+
end
|
241
|
+
|
242
|
+
f.puts
|
243
|
+
f.puts("### #{name}")
|
244
|
+
if description
|
245
|
+
f.puts
|
246
|
+
f.puts(description)
|
264
247
|
end
|
248
|
+
f.puts
|
249
|
+
html = view_context.render(inline: code)
|
250
|
+
html.scan(/class="([^"]*)"/) do |classnames|
|
251
|
+
classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
|
252
|
+
end
|
253
|
+
f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
|
254
|
+
f.puts
|
255
|
+
f.puts("```erb")
|
256
|
+
f.puts(code.to_s)
|
257
|
+
f.puts("```")
|
265
258
|
end
|
266
259
|
end
|
267
260
|
end
|
268
261
|
|
262
|
+
unless errors.empty?
|
263
|
+
puts "==============================================="
|
264
|
+
puts "===================== ERRORS =================="
|
265
|
+
puts "===============================================\n\n"
|
266
|
+
puts JSON.pretty_generate(errors)
|
267
|
+
puts "\n\n==============================================="
|
268
|
+
puts "==============================================="
|
269
|
+
puts "==============================================="
|
270
|
+
|
271
|
+
raise
|
272
|
+
end
|
273
|
+
|
269
274
|
File.open("static/classes.yml", "w") do |f|
|
270
275
|
f.puts YAML.dump(classes_found_in_examples.sort.uniq)
|
271
276
|
end
|
@@ -293,11 +298,6 @@ namespace :docs do
|
|
293
298
|
|
294
299
|
puts "Markdown compiled."
|
295
300
|
|
296
|
-
if components_without_examples.any?
|
297
|
-
puts
|
298
|
-
puts "The following components have no examples defined: #{components_without_examples.map(&:name).join(', ')}. Consider adding an example?"
|
299
|
-
end
|
300
|
-
|
301
301
|
if components_needing_docs.any?
|
302
302
|
puts
|
303
303
|
puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(', ')}"
|
@@ -307,6 +307,8 @@ namespace :docs do
|
|
307
307
|
task :preview do
|
308
308
|
registry = generate_yard_registry
|
309
309
|
|
310
|
+
FileUtils.rm_rf("demo/test/components/previews/primer/docs/")
|
311
|
+
|
310
312
|
components = Primer::Component.descendants
|
311
313
|
|
312
314
|
# Generate previews from documentation examples
|
@@ -346,6 +348,7 @@ namespace :docs do
|
|
346
348
|
end
|
347
349
|
|
348
350
|
def generate_yard_registry
|
351
|
+
ENV["SKIP_STORYBOOK_PRELOAD"] = "1"
|
349
352
|
require File.expand_path("./../../demo/config/environment.rb", __dir__)
|
350
353
|
require "primer/view_components"
|
351
354
|
require "yard/docs_helper"
|
@@ -361,6 +364,7 @@ namespace :docs do
|
|
361
364
|
# Custom tags for yard
|
362
365
|
YARD::Tags::Library.define_tag("Accessibility", :accessibility)
|
363
366
|
YARD::Tags::Library.define_tag("Deprecation", :deprecation)
|
367
|
+
YARD::Tags::Library.define_tag("Parameter", :param, :with_types_name_and_default)
|
364
368
|
|
365
369
|
puts "Building YARD documentation."
|
366
370
|
Rake::Task["yard"].execute
|
@@ -369,4 +373,69 @@ namespace :docs do
|
|
369
373
|
registry.load!(".yardoc")
|
370
374
|
registry
|
371
375
|
end
|
376
|
+
|
377
|
+
def pretty_default_value(tag, component)
|
378
|
+
params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
|
379
|
+
default = tag.defaults&.first || params&.second
|
380
|
+
|
381
|
+
return "N/A" unless default
|
382
|
+
|
383
|
+
constant_name = "#{component.name}::#{default}"
|
384
|
+
constant_value = default.safe_constantize || constant_name.safe_constantize
|
385
|
+
|
386
|
+
return pretty_value(default) if constant_value.nil?
|
387
|
+
|
388
|
+
pretty_value(constant_value)
|
389
|
+
end
|
390
|
+
|
391
|
+
def status_module_and_short_name(component)
|
392
|
+
name_with_status = component.name.gsub(/Primer::|Component/, "")
|
393
|
+
|
394
|
+
m = name_with_status.match(/(?<status>Beta|Alpha|Deprecated)?(::)?(?<name>.*)/)
|
395
|
+
[m[:status]&.downcase, m[:name].gsub("::", "")]
|
396
|
+
end
|
397
|
+
|
398
|
+
def docs_metadata(component)
|
399
|
+
(status_module, short_name) = status_module_and_short_name(component)
|
400
|
+
status_path = status_module.nil? ? "" : "#{status_module}/"
|
401
|
+
status = component.status.to_s
|
402
|
+
|
403
|
+
{
|
404
|
+
title: short_name,
|
405
|
+
status: status.capitalize,
|
406
|
+
source: source_url(component),
|
407
|
+
storybook: storybook_url(component),
|
408
|
+
path: "docs/content/components/#{status_path}#{short_name.downcase}.md",
|
409
|
+
example_path: example_path(component),
|
410
|
+
require_js_path: require_js_path(component)
|
411
|
+
}
|
412
|
+
end
|
413
|
+
|
414
|
+
def source_url(component)
|
415
|
+
path = component.name.split("::").map(&:underscore).join("/")
|
416
|
+
|
417
|
+
"https://github.com/primer/view_components/tree/main/app/components/#{path}.rb"
|
418
|
+
end
|
419
|
+
|
420
|
+
def storybook_url(component)
|
421
|
+
path = component.name.split("::").map { |n| n.underscore.dasherize }.join("-")
|
422
|
+
|
423
|
+
"https://primer.style/view-components/stories/?path=/story/#{path}"
|
424
|
+
end
|
425
|
+
|
426
|
+
def example_path(component)
|
427
|
+
example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
|
428
|
+
example_path = "../#{example_path}" if status_module?(component)
|
429
|
+
example_path
|
430
|
+
end
|
431
|
+
|
432
|
+
def require_js_path(component)
|
433
|
+
require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
|
434
|
+
require_js_path = "../#{require_js_path}" if status_module?(component)
|
435
|
+
require_js_path
|
436
|
+
end
|
437
|
+
|
438
|
+
def status_module?(component)
|
439
|
+
(%w[Alpha Beta] & component.name.split("::")).any?
|
440
|
+
end
|
372
441
|
end
|