primer_view_components 0.0.41 → 0.0.46
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 +265 -1
- 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/auto_complete.rb +99 -42
- data/app/components/primer/auto_complete/auto_complete.html.erb +1 -0
- data/app/components/primer/avatar_stack_component.rb +7 -1
- data/app/components/primer/base_component.rb +62 -26
- data/app/components/primer/beta/text.rb +27 -0
- data/app/components/primer/blankslate_component.html.erb +1 -0
- data/app/components/primer/blankslate_component.rb +64 -45
- data/app/components/primer/border_box_component.rb +3 -0
- data/app/components/primer/button_component.rb +3 -2
- data/app/components/primer/button_group.rb +1 -1
- data/app/components/primer/clipboard_copy.rb +25 -7
- data/app/components/primer/component.rb +5 -1
- 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/hidden_text_expander.rb +2 -2
- data/app/components/primer/icon_button.rb +1 -1
- data/app/components/primer/image_crop.rb +2 -2
- data/app/components/primer/markdown.rb +6 -2
- 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 +4 -3
- data/app/components/primer/popover_component.rb +2 -2
- data/app/components/primer/primer.d.ts +1 -0
- data/app/components/primer/primer.js +1 -0
- data/app/components/primer/primer.ts +1 -0
- data/app/components/primer/spinner_component.rb +2 -0
- data/app/components/primer/tab_nav_component.html.erb +4 -2
- data/app/components/primer/tab_nav_component.rb +48 -6
- data/app/components/primer/tooltip.rb +1 -1
- data/app/components/primer/truncate.rb +6 -2
- data/app/components/primer/underline_nav_component.html.erb +1 -1
- data/app/components/primer/underline_nav_component.rb +27 -5
- data/app/lib/primer/tabbed_component_helper.rb +2 -2
- data/{app/lib → lib}/primer/classify.rb +41 -35
- data/{app/lib → lib}/primer/classify/cache.rb +16 -35
- 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/lib/primer/classify/grid.rb +45 -0
- data/lib/primer/classify/utilities.rb +137 -0
- data/lib/primer/classify/utilities.yml +1271 -0
- data/lib/primer/view_components.rb +1 -0
- data/lib/primer/view_components/engine.rb +2 -0
- data/lib/primer/view_components/linters.rb +3 -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 +39 -0
- data/lib/primer/view_components/linters/flash_component_migration_counter.rb +16 -0
- data/lib/primer/view_components/linters/helpers.rb +191 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/tasks/docs.rake +180 -108
- data/lib/tasks/utilities.rake +105 -0
- data/lib/yard/docs_helper.rb +12 -2
- data/static/statuses.json +7 -5
- metadata +50 -20
- 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
@@ -13,7 +13,9 @@ module Primer
|
|
13
13
|
]
|
14
14
|
|
15
15
|
config.primer_view_components = ActiveSupport::OrderedOptions.new
|
16
|
+
|
16
17
|
config.primer_view_components.force_functional_colors = true
|
18
|
+
config.primer_view_components.force_system_arguments = false
|
17
19
|
config.primer_view_components.silence_deprecations = false
|
18
20
|
|
19
21
|
initializer "primer_view_components.assets" do |app|
|
@@ -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
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helpers"
|
4
|
+
require_relative "argument_mappers/button"
|
5
|
+
|
6
|
+
module ERBLint
|
7
|
+
module Linters
|
8
|
+
# Counts the number of times a HTML button is used instead of the component.
|
9
|
+
class ButtonComponentMigrationCounter < Linter
|
10
|
+
include Helpers
|
11
|
+
|
12
|
+
TAGS = %w[button summary a].freeze
|
13
|
+
CLASSES = %w[btn btn-link].freeze
|
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
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helpers"
|
4
|
+
|
5
|
+
module ERBLint
|
6
|
+
module Linters
|
7
|
+
# Counts the number of times a HTML flash is used instead of the component.
|
8
|
+
class FlashComponentMigrationCounter < Linter
|
9
|
+
include Helpers
|
10
|
+
|
11
|
+
TAGS = %w[div].freeze
|
12
|
+
CLASSES = %w[flash].freeze
|
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
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "openssl"
|
5
|
+
|
6
|
+
module ERBLint
|
7
|
+
module Linters
|
8
|
+
# Helper methods for linting ERB.
|
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
|
+
def self.included(base)
|
17
|
+
base.include(ERBLint::LinterRegistry)
|
18
|
+
|
19
|
+
define_method "run" do |processed_source|
|
20
|
+
@offenses_not_corrected = 0
|
21
|
+
tags = tags(processed_source)
|
22
|
+
tag_tree = build_tag_tree(tags)
|
23
|
+
|
24
|
+
tags.each do |tag|
|
25
|
+
next if tag.closing?
|
26
|
+
next unless self.class::TAGS&.include?(tag.name)
|
27
|
+
|
28
|
+
classes = tag.attributes["class"]&.value&.split(" ") || []
|
29
|
+
|
30
|
+
tag_tree[tag][:offense] = false
|
31
|
+
|
32
|
+
next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
|
33
|
+
|
34
|
+
args = map_arguments(tag)
|
35
|
+
correction = correction(args)
|
36
|
+
|
37
|
+
tag_tree[tag][:offense] = true
|
38
|
+
tag_tree[tag][:correctable] = !correction.nil?
|
39
|
+
tag_tree[tag][:message] = message(args)
|
40
|
+
tag_tree[tag][:correction] = correction
|
41
|
+
end
|
42
|
+
|
43
|
+
tag_tree.each do |tag, h|
|
44
|
+
next unless h[:offense]
|
45
|
+
|
46
|
+
# We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
|
47
|
+
if h[:correctable]
|
48
|
+
add_offense(tag.loc, h[:message], h[:correction])
|
49
|
+
add_offense(h[:closing].loc, h[:message], "<% end %>")
|
50
|
+
else
|
51
|
+
@offenses_not_corrected += 1
|
52
|
+
generate_offense(self.class, processed_source, tag, h[:message])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
counter_correct?(processed_source)
|
57
|
+
end
|
58
|
+
|
59
|
+
define_method "autocorrect" do |processed_source, offense|
|
60
|
+
return unless offense.context
|
61
|
+
|
62
|
+
lambda do |corrector|
|
63
|
+
if offense.context.include?(counter_disable)
|
64
|
+
correct_counter(corrector, processed_source, offense)
|
65
|
+
else
|
66
|
+
corrector.replace(offense.source_range, offense.context)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Override this function to convert the HTML element attributes to argument for a component.
|
75
|
+
#
|
76
|
+
# @return [Hash] if possible to map all attributes to arguments.
|
77
|
+
# @return [Nil] if cannot map to arguments.
|
78
|
+
def map_arguments(_tag)
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Override this function to define how to autocorrect an element to a component.
|
83
|
+
#
|
84
|
+
# @return [String] with the text to replace the HTML element if possible to correct.
|
85
|
+
# @return [Nil] if cannot correct element.
|
86
|
+
def correction(_tag)
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# Override this function to customize the linter message.
|
91
|
+
#
|
92
|
+
# @return [String] message to show on linter error.
|
93
|
+
def message(_tag)
|
94
|
+
self.class::MESSAGE
|
95
|
+
end
|
96
|
+
|
97
|
+
def counter_disable
|
98
|
+
"erblint:counter #{self.class.name.demodulize}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def correct_counter(corrector, processed_source, offense)
|
102
|
+
if processed_source.file_content.include?(counter_disable)
|
103
|
+
# update the counter if exists
|
104
|
+
corrector.replace(offense.source_range, offense.context)
|
105
|
+
else
|
106
|
+
# add comment with counter if none
|
107
|
+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
|
112
|
+
# From the tags, we build a structured tree which represents the tag hierarchy.
|
113
|
+
# With this, we are able to know where the tags start and end.
|
114
|
+
def build_tag_tree(tags)
|
115
|
+
tag_tree = {}
|
116
|
+
current_opened_tag = nil
|
117
|
+
|
118
|
+
tags.each do |tag|
|
119
|
+
if tag.closing?
|
120
|
+
if current_opened_tag && tag.name == current_opened_tag.name
|
121
|
+
tag_tree[current_opened_tag][:closing] = tag
|
122
|
+
current_opened_tag = tag_tree[current_opened_tag][:parent]
|
123
|
+
end
|
124
|
+
|
125
|
+
next
|
126
|
+
end
|
127
|
+
|
128
|
+
self_closing = self_closing?(tag)
|
129
|
+
|
130
|
+
tag_tree[tag] = {
|
131
|
+
closing: self_closing ? tag : nil,
|
132
|
+
parent: current_opened_tag
|
133
|
+
}
|
134
|
+
|
135
|
+
current_opened_tag = tag unless self_closing
|
136
|
+
end
|
137
|
+
|
138
|
+
tag_tree
|
139
|
+
end
|
140
|
+
|
141
|
+
def self_closing?(tag)
|
142
|
+
tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
|
143
|
+
end
|
144
|
+
|
145
|
+
def tags(processed_source)
|
146
|
+
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
|
147
|
+
end
|
148
|
+
|
149
|
+
def counter_correct?(processed_source)
|
150
|
+
comment_node = nil
|
151
|
+
expected_count = 0
|
152
|
+
rule_name = self.class.name.match(/:?:?(\w+)\Z/)[1]
|
153
|
+
|
154
|
+
processed_source.parser.ast.descendants(:erb).each do |node|
|
155
|
+
indicator_node, _, code_node, = *node
|
156
|
+
indicator = indicator_node&.loc&.source
|
157
|
+
comment = code_node&.loc&.source&.strip
|
158
|
+
|
159
|
+
if indicator == "#" && comment.start_with?("erblint:count") && comment.match(rule_name)
|
160
|
+
comment_node = node
|
161
|
+
expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
if @offenses_not_corrected.zero?
|
166
|
+
# have to adjust to get `\n` so we delete the whole line
|
167
|
+
add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:count comment for #{rule_name}", "") if comment_node
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
first_offense = @offenses[0]
|
172
|
+
|
173
|
+
if comment_node.nil?
|
174
|
+
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} %>")
|
175
|
+
elsif expected_count != @offenses_not_corrected
|
176
|
+
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} %>")
|
177
|
+
# the only offenses remaining are not autocorrectable, so we can ignore them
|
178
|
+
elsif expected_count == @offenses_not_corrected && @offenses.size == @offenses_not_corrected
|
179
|
+
clear_offenses
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
|
184
|
+
message ||= klass::MESSAGE
|
185
|
+
klass_name = klass.name.demodulize
|
186
|
+
offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
|
187
|
+
add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/lib/tasks/docs.rake
CHANGED
@@ -20,32 +20,12 @@ namespace :docs do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
task :build do
|
23
|
-
|
24
|
-
require "primer/view_components"
|
25
|
-
require "yard/docs_helper"
|
26
|
-
require "view_component/test_helpers"
|
27
|
-
include ViewComponent::TestHelpers
|
28
|
-
include Primer::ViewHelper
|
29
|
-
include YARD::DocsHelper
|
30
|
-
|
31
|
-
Dir["./app/components/primer/**/*.rb"].sort.each { |file| require file }
|
32
|
-
|
33
|
-
YARD::Rake::YardocTask.new
|
34
|
-
|
35
|
-
# Custom tags for yard
|
36
|
-
YARD::Tags::Library.define_tag("Accessibility", :accessibility)
|
37
|
-
YARD::Tags::Library.define_tag("Deprecation", :deprecation)
|
38
|
-
|
39
|
-
puts "Building YARD documentation."
|
40
|
-
Rake::Task["yard"].execute
|
23
|
+
registry = generate_yard_registry
|
41
24
|
|
42
25
|
puts "Converting YARD documentation to Markdown files."
|
43
26
|
|
44
27
|
# Rails controller for rendering arbitrary ERB
|
45
28
|
view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context
|
46
|
-
|
47
|
-
registry = YARD::RegistryStore.new
|
48
|
-
registry.load!(".yardoc")
|
49
29
|
components = [
|
50
30
|
Primer::Image,
|
51
31
|
Primer::LocalTime,
|
@@ -63,12 +43,12 @@ namespace :docs do
|
|
63
43
|
Primer::BreadcrumbComponent,
|
64
44
|
Primer::ButtonComponent,
|
65
45
|
Primer::ButtonGroup,
|
66
|
-
Primer::
|
46
|
+
Primer::Alpha::ButtonMarketing,
|
67
47
|
Primer::ClipboardCopy,
|
68
48
|
Primer::CloseButton,
|
69
49
|
Primer::CounterComponent,
|
70
50
|
Primer::DetailsComponent,
|
71
|
-
Primer::
|
51
|
+
Primer::Dropdown,
|
72
52
|
Primer::DropdownMenuComponent,
|
73
53
|
Primer::FlashComponent,
|
74
54
|
Primer::FlexComponent,
|
@@ -89,7 +69,7 @@ namespace :docs do
|
|
89
69
|
Primer::SubheadComponent,
|
90
70
|
Primer::TabContainerComponent,
|
91
71
|
Primer::TabNavComponent,
|
92
|
-
Primer::
|
72
|
+
Primer::Beta::Text,
|
93
73
|
Primer::TimeAgoComponent,
|
94
74
|
Primer::TimelineItemComponent,
|
95
75
|
Primer::Tooltip,
|
@@ -98,6 +78,7 @@ namespace :docs do
|
|
98
78
|
]
|
99
79
|
|
100
80
|
js_components = [
|
81
|
+
Primer::Dropdown,
|
101
82
|
Primer::LocalTime,
|
102
83
|
Primer::ImageCrop,
|
103
84
|
Primer::AutoComplete,
|
@@ -111,10 +92,11 @@ namespace :docs do
|
|
111
92
|
all_components = Primer::Component.descendants - [Primer::BaseComponent]
|
112
93
|
components_needing_docs = all_components - components
|
113
94
|
|
114
|
-
components_without_examples = []
|
115
95
|
args_for_components = []
|
116
96
|
classes_found_in_examples = []
|
117
97
|
|
98
|
+
errors = []
|
99
|
+
|
118
100
|
components.each do |component|
|
119
101
|
documentation = registry.get(component.name)
|
120
102
|
|
@@ -133,6 +115,8 @@ namespace :docs do
|
|
133
115
|
f.puts
|
134
116
|
f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'")
|
135
117
|
|
118
|
+
initialize_method = documentation.meths.find(&:constructor?)
|
119
|
+
|
136
120
|
if js_components.include?(component)
|
137
121
|
f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'")
|
138
122
|
f.puts
|
@@ -144,97 +128,68 @@ namespace :docs do
|
|
144
128
|
f.puts
|
145
129
|
f.puts(view_context.render(inline: documentation.base_docstring))
|
146
130
|
|
147
|
-
if documentation.tags(:
|
131
|
+
if documentation.tags(:deprecated).any?
|
148
132
|
f.puts
|
149
|
-
f.puts("##
|
150
|
-
documentation.tags(:
|
133
|
+
f.puts("## Deprecation")
|
134
|
+
documentation.tags(:deprecated).each do |tag|
|
151
135
|
f.puts
|
152
136
|
f.puts view_context.render(inline: tag.text)
|
153
137
|
end
|
154
138
|
end
|
155
139
|
|
156
|
-
if documentation.tags(:
|
140
|
+
if documentation.tags(:accessibility).any?
|
157
141
|
f.puts
|
158
|
-
f.puts("##
|
159
|
-
documentation.tags(:
|
142
|
+
f.puts("## Accessibility")
|
143
|
+
documentation.tags(:accessibility).each do |tag|
|
160
144
|
f.puts
|
161
145
|
f.puts view_context.render(inline: tag.text)
|
162
146
|
end
|
163
147
|
end
|
164
148
|
|
165
|
-
|
149
|
+
params = initialize_method.tags(:param)
|
166
150
|
|
167
|
-
|
168
|
-
f.puts
|
169
|
-
f.puts("## Examples")
|
170
|
-
else
|
171
|
-
components_without_examples << component
|
172
|
-
end
|
151
|
+
errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
|
173
152
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
153
|
+
f.puts
|
154
|
+
f.puts("## Arguments")
|
155
|
+
f.puts
|
156
|
+
f.puts("| Name | Type | Default | Description |")
|
157
|
+
f.puts("| :- | :- | :- | :- |")
|
158
|
+
|
159
|
+
docummented_params = params.map(&:name)
|
160
|
+
component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
|
161
|
+
|
162
|
+
if (docummented_params & component_params).size != component_params.size
|
163
|
+
err = { arguments: {} }
|
164
|
+
(component_params - docummented_params).each do |arg|
|
165
|
+
err[:arguments][arg] = "Not documented"
|
186
166
|
end
|
187
|
-
|
188
|
-
|
189
|
-
f.puts("```erb")
|
190
|
-
f.puts(tag.text.to_s)
|
191
|
-
f.puts("```")
|
167
|
+
|
168
|
+
errors << { component.name => err }
|
192
169
|
end
|
193
170
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
f.puts("## Arguments")
|
198
|
-
f.puts
|
199
|
-
f.puts("| Name | Type | Default | Description |")
|
200
|
-
f.puts("| :- | :- | :- | :- |")
|
201
|
-
|
202
|
-
args = []
|
203
|
-
params.each do |tag|
|
204
|
-
params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
|
205
|
-
|
206
|
-
default =
|
207
|
-
if params && params[1]
|
208
|
-
constant_name = "#{component.name}::#{params[1]}"
|
209
|
-
constant_value = constant_name.safe_constantize
|
210
|
-
if constant_value.nil?
|
211
|
-
pretty_value(params[1])
|
212
|
-
else
|
213
|
-
pretty_value(constant_value)
|
214
|
-
end
|
215
|
-
else
|
216
|
-
"N/A"
|
217
|
-
end
|
218
|
-
|
219
|
-
args << {
|
220
|
-
"name" => tag.name,
|
221
|
-
"type" => tag.types.join(", "),
|
222
|
-
"default" => default,
|
223
|
-
"description" => view_context.render(inline: tag.text)
|
224
|
-
}
|
225
|
-
|
226
|
-
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |")
|
227
|
-
end
|
171
|
+
args = []
|
172
|
+
params.each do |tag|
|
173
|
+
default_value = pretty_default_value(tag, component)
|
228
174
|
|
229
|
-
|
230
|
-
"
|
231
|
-
"
|
232
|
-
"
|
175
|
+
args << {
|
176
|
+
"name" => tag.name,
|
177
|
+
"type" => tag.types.join(", "),
|
178
|
+
"default" => default_value,
|
179
|
+
"description" => view_context.render(inline: tag.text.squish)
|
233
180
|
}
|
234
181
|
|
235
|
-
|
182
|
+
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
|
236
183
|
end
|
237
184
|
|
185
|
+
component_args = {
|
186
|
+
"component" => short_name,
|
187
|
+
"source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb",
|
188
|
+
"parameters" => args
|
189
|
+
}
|
190
|
+
|
191
|
+
args_for_components << component_args
|
192
|
+
|
238
193
|
# Slots V2 docs
|
239
194
|
slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
|
240
195
|
|
@@ -259,22 +214,61 @@ namespace :docs do
|
|
259
214
|
end
|
260
215
|
|
261
216
|
param_tags.each do |tag|
|
262
|
-
|
217
|
+
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
263
221
|
|
264
|
-
|
265
|
-
if params && params[1]
|
266
|
-
"`#{params[1]}`"
|
267
|
-
else
|
268
|
-
"N/A"
|
269
|
-
end
|
222
|
+
errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
|
270
223
|
|
271
|
-
|
272
|
-
|
224
|
+
f.puts
|
225
|
+
f.puts("## Examples")
|
226
|
+
|
227
|
+
initialize_method.tags(:example).each do |tag|
|
228
|
+
name = tag.name
|
229
|
+
description = nil
|
230
|
+
code = nil
|
231
|
+
|
232
|
+
if tag.text.include?("@description")
|
233
|
+
splitted = tag.text.split(/@description|@code/)
|
234
|
+
description = splitted.second.gsub(/^[ \t]{2}/, "").strip
|
235
|
+
code = splitted.last.gsub(/^[ \t]{2}/, "").strip
|
236
|
+
else
|
237
|
+
code = tag.text
|
238
|
+
end
|
239
|
+
|
240
|
+
f.puts
|
241
|
+
f.puts("### #{name}")
|
242
|
+
if description
|
243
|
+
f.puts
|
244
|
+
f.puts(description)
|
245
|
+
end
|
246
|
+
f.puts
|
247
|
+
html = view_context.render(inline: code)
|
248
|
+
html.scan(/class="([^"]*)"/) do |classnames|
|
249
|
+
classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
|
273
250
|
end
|
251
|
+
f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
|
252
|
+
f.puts
|
253
|
+
f.puts("```erb")
|
254
|
+
f.puts(code.to_s)
|
255
|
+
f.puts("```")
|
274
256
|
end
|
275
257
|
end
|
276
258
|
end
|
277
259
|
|
260
|
+
unless errors.empty?
|
261
|
+
puts "==============================================="
|
262
|
+
puts "===================== ERRORS =================="
|
263
|
+
puts "===============================================\n\n"
|
264
|
+
puts JSON.pretty_generate(errors)
|
265
|
+
puts "\n\n==============================================="
|
266
|
+
puts "==============================================="
|
267
|
+
puts "==============================================="
|
268
|
+
|
269
|
+
raise
|
270
|
+
end
|
271
|
+
|
278
272
|
File.open("static/classes.yml", "w") do |f|
|
279
273
|
f.puts YAML.dump(classes_found_in_examples.sort.uniq)
|
280
274
|
end
|
@@ -302,14 +296,92 @@ namespace :docs do
|
|
302
296
|
|
303
297
|
puts "Markdown compiled."
|
304
298
|
|
305
|
-
if components_without_examples.any?
|
306
|
-
puts
|
307
|
-
puts "The following components have no examples defined: #{components_without_examples.map(&:name).join(', ')}. Consider adding an example?"
|
308
|
-
end
|
309
|
-
|
310
299
|
if components_needing_docs.any?
|
311
300
|
puts
|
312
301
|
puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(', ')}"
|
313
302
|
end
|
314
303
|
end
|
304
|
+
|
305
|
+
task :preview do
|
306
|
+
registry = generate_yard_registry
|
307
|
+
|
308
|
+
FileUtils.rm_rf("demo/test/components/previews/primer/docs/")
|
309
|
+
|
310
|
+
components = Primer::Component.descendants
|
311
|
+
|
312
|
+
# Generate previews from documentation examples
|
313
|
+
components.each do |component|
|
314
|
+
documentation = registry.get(component.name)
|
315
|
+
short_name = component.name.gsub(/Primer|::/, "")
|
316
|
+
initialize_method = documentation.meths.find(&:constructor?)
|
317
|
+
|
318
|
+
next unless initialize_method.tags(:example).any?
|
319
|
+
|
320
|
+
yard_example_tags = initialize_method.tags(:example)
|
321
|
+
|
322
|
+
path = Pathname.new("demo/test/components/previews/primer/docs/#{short_name.underscore}_preview.rb")
|
323
|
+
path.dirname.mkdir unless path.dirname.exist?
|
324
|
+
|
325
|
+
File.open(path, "w") do |f|
|
326
|
+
f.puts("module Primer")
|
327
|
+
f.puts(" module Docs")
|
328
|
+
f.puts(" class #{short_name}Preview < ViewComponent::Preview")
|
329
|
+
|
330
|
+
yard_example_tags.each_with_index do |tag, index|
|
331
|
+
method_name = tag.name.split("|").first.downcase.parameterize.underscore
|
332
|
+
f.puts(" def #{method_name}; end")
|
333
|
+
f.puts unless index == yard_example_tags.size - 1
|
334
|
+
path = Pathname.new("demo/test/components/previews/primer/docs/#{short_name.underscore}_preview/#{method_name}.html.erb")
|
335
|
+
path.dirname.mkdir unless path.dirname.exist?
|
336
|
+
File.open(path, "w") do |view_file|
|
337
|
+
view_file.puts(tag.text.to_s)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
f.puts(" end")
|
342
|
+
f.puts(" end")
|
343
|
+
f.puts("end")
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def generate_yard_registry
|
349
|
+
require File.expand_path("./../../demo/config/environment.rb", __dir__)
|
350
|
+
require "primer/view_components"
|
351
|
+
require "yard/docs_helper"
|
352
|
+
require "view_component/test_helpers"
|
353
|
+
include ViewComponent::TestHelpers
|
354
|
+
include Primer::ViewHelper
|
355
|
+
include YARD::DocsHelper
|
356
|
+
|
357
|
+
Dir["./app/components/primer/**/*.rb"].sort.each { |file| require file }
|
358
|
+
|
359
|
+
YARD::Rake::YardocTask.new
|
360
|
+
|
361
|
+
# Custom tags for yard
|
362
|
+
YARD::Tags::Library.define_tag("Accessibility", :accessibility)
|
363
|
+
YARD::Tags::Library.define_tag("Deprecation", :deprecation)
|
364
|
+
YARD::Tags::Library.define_tag("Parameter", :param, :with_types_name_and_default)
|
365
|
+
|
366
|
+
puts "Building YARD documentation."
|
367
|
+
Rake::Task["yard"].execute
|
368
|
+
|
369
|
+
registry = YARD::RegistryStore.new
|
370
|
+
registry.load!(".yardoc")
|
371
|
+
registry
|
372
|
+
end
|
373
|
+
|
374
|
+
def pretty_default_value(tag, component)
|
375
|
+
params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
|
376
|
+
default = tag.defaults&.first || params&.second
|
377
|
+
|
378
|
+
return "N/A" unless default
|
379
|
+
|
380
|
+
constant_name = "#{component.name}::#{default}"
|
381
|
+
constant_value = default.safe_constantize || constant_name.safe_constantize
|
382
|
+
|
383
|
+
return pretty_value(default) if constant_value.nil?
|
384
|
+
|
385
|
+
pretty_value(constant_value)
|
386
|
+
end
|
315
387
|
end
|