primer_view_components 0.0.39 → 0.0.44
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 +269 -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/auto_complete.rb +99 -41
- data/app/components/primer/auto_complete/auto_complete.html.erb +1 -0
- data/app/components/primer/avatar_stack_component.rb +7 -4
- data/app/components/primer/base_component.rb +17 -7
- 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 +68 -49
- data/app/components/primer/button_component.rb +3 -2
- data/app/components/primer/button_group.rb +2 -2
- data/app/components/primer/clipboard_copy_component.js +13 -2
- data/app/components/primer/clipboard_copy_component.ts +15 -2
- data/app/components/primer/component.rb +6 -1
- data/app/components/primer/counter_component.rb +6 -1
- data/app/components/primer/details_component.rb +12 -1
- 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/flash_component.rb +2 -2
- data/app/components/primer/flex_component.rb +27 -0
- data/app/components/primer/flex_item_component.rb +1 -1
- data/app/components/primer/heading_component.rb +11 -18
- data/app/components/primer/hidden_text_expander.rb +3 -3
- data/app/components/primer/icon_button.rb +20 -3
- data/app/components/primer/image.rb +46 -0
- data/app/components/primer/image_crop.d.ts +1 -0
- data/app/components/primer/image_crop.html.erb +12 -0
- data/app/components/primer/image_crop.js +1 -0
- data/app/components/primer/image_crop.rb +36 -0
- data/app/components/primer/image_crop.ts +1 -0
- data/app/components/primer/label_component.rb +6 -2
- data/app/components/primer/local_time.d.ts +1 -0
- data/app/components/primer/local_time.js +1 -0
- data/app/components/primer/local_time.rb +59 -0
- data/app/components/primer/local_time.ts +1 -0
- data/app/components/primer/{markdown_component.rb → markdown.rb} +11 -6
- data/app/components/primer/navigation/tab_component.rb +10 -3
- data/app/components/primer/octicon_component.html.erb +7 -0
- data/app/components/primer/octicon_component.rb +25 -15
- data/app/components/primer/octicon_symbols_component.html.erb +3 -0
- data/app/components/primer/octicon_symbols_component.rb +61 -0
- data/app/components/primer/primer.d.ts +3 -0
- data/app/components/primer/primer.js +3 -0
- data/app/components/primer/primer.ts +3 -0
- data/app/components/primer/spinner_component.rb +4 -2
- data/app/components/primer/subhead_component.rb +34 -4
- data/app/components/primer/tab_nav_component.html.erb +5 -1
- data/app/components/primer/tab_nav_component.rb +62 -9
- data/app/components/primer/{tooltip_component.rb → tooltip.rb} +10 -8
- 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 +17 -1
- data/app/lib/primer/classify.rb +21 -8
- data/app/lib/primer/classify/cache.rb +16 -1
- data/app/lib/primer/classify/grid.rb +45 -0
- data/app/lib/primer/octicon/cache.rb +4 -0
- data/app/lib/primer/tabbed_component_helper.rb +2 -2
- data/app/lib/primer/view_helper.rb +2 -1
- data/lib/primer/view_components.rb +1 -1
- 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 +46 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +35 -0
- data/lib/primer/view_components/linters/flash_component_migration_counter.rb +16 -0
- data/lib/primer/view_components/linters/helpers.rb +93 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/tasks/coverage.rake +14 -0
- data/lib/tasks/docs.rake +387 -0
- data/lib/tasks/statuses.rake +12 -0
- data/lib/yard/docs_helper.rb +67 -0
- data/static/statuses.json +56 -1
- metadata +72 -13
- 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 -22
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Primer
|
|
4
|
+
class Classify
|
|
5
|
+
# Handler for PrimerCSS grid classes.
|
|
6
|
+
class Grid
|
|
7
|
+
extend Primer::FetchOrFallbackHelper
|
|
8
|
+
|
|
9
|
+
CONTAINER_KEY = :container
|
|
10
|
+
CONTAINER_VALUES = [:xl, :lg, :md, :sm].freeze
|
|
11
|
+
|
|
12
|
+
CLEARFIX_KEY = :clearfix
|
|
13
|
+
CLEARFIX_VALUES = [true, false].freeze
|
|
14
|
+
|
|
15
|
+
COL_KEY = :col
|
|
16
|
+
COL_VALUES = (1..12).to_a.freeze
|
|
17
|
+
|
|
18
|
+
KEYS = [CONTAINER_KEY, CLEARFIX_KEY, COL_KEY].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def classes(key, value, breakpoint)
|
|
22
|
+
send(key, value, breakpoint)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def container(value, _breakpoint)
|
|
28
|
+
val = fetch_or_fallback(CONTAINER_VALUES, value)
|
|
29
|
+
|
|
30
|
+
"container-#{val}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clearfix(value, _breakpoint)
|
|
34
|
+
"clearfix" if fetch_or_fallback(CLEARFIX_VALUES, value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def col(value, breakpoint)
|
|
38
|
+
val = fetch_or_fallback(COL_VALUES, value.to_i)
|
|
39
|
+
|
|
40
|
+
"col#{breakpoint}-#{val}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -9,6 +9,10 @@ module Primer
|
|
|
9
9
|
PRELOADED_ICONS = [:alert, :check, :"chevron-down", :clippy, :clock, :"dot-fill", :info, :"kebab-horizontal", :link, :lock, :mail, :pencil, :plus, :question, :repo, :search, :"shield-lock", :star, :trash, :x].freeze
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
|
+
def get_key(symbol:, size:, width: nil, height: nil)
|
|
13
|
+
[symbol, size, width, height].join("_")
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
def read(key)
|
|
13
17
|
LOOKUP[key]
|
|
14
18
|
end
|
|
@@ -20,10 +20,10 @@ module Primer
|
|
|
20
20
|
with_panel ? :div : :nav
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def wrapper
|
|
23
|
+
def wrapper(**system_arguments)
|
|
24
24
|
return yield unless @with_panel
|
|
25
25
|
|
|
26
|
-
render Primer::TabContainerComponent.new do
|
|
26
|
+
render Primer::TabContainerComponent.new(**system_arguments) do
|
|
27
27
|
yield
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -8,7 +8,8 @@ module Primer
|
|
|
8
8
|
HELPERS = {
|
|
9
9
|
octicon: "Primer::OcticonComponent",
|
|
10
10
|
heading: "Primer::HeadingComponent",
|
|
11
|
-
time_ago: "Primer::TimeAgoComponent"
|
|
11
|
+
time_ago: "Primer::TimeAgoComponent",
|
|
12
|
+
image: "Primer::Image"
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
HELPERS.each do |name, component|
|
|
@@ -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,46 @@
|
|
|
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_PARAETERS = %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_PARAETERS)
|
|
33
|
+
# if attribute has no value_node, it means it is a boolean attribute.
|
|
34
|
+
{ "\"#{attr_name}\"" => attribute.value_node ? attribute.value.to_json : true }
|
|
35
|
+
else
|
|
36
|
+
raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def attr_name
|
|
41
|
+
attribute.name
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
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 message(tag)
|
|
25
|
+
args = map_arguments(tag)
|
|
26
|
+
|
|
27
|
+
return MESSAGE if args.nil?
|
|
28
|
+
|
|
29
|
+
msg = "#{MESSAGE}\n\nTry using:\n\n<%= render Primer::ButtonComponent.new"
|
|
30
|
+
msg += "(#{args})" if args.present?
|
|
31
|
+
"#{msg} %>\n\nInstead of:\n"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
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,93 @@
|
|
|
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
|
+
def self.included(base)
|
|
11
|
+
base.include(ERBLint::LinterRegistry)
|
|
12
|
+
|
|
13
|
+
define_method "run" do |processed_source|
|
|
14
|
+
tags(processed_source).each do |tag|
|
|
15
|
+
next if tag.closing?
|
|
16
|
+
next unless self.class::TAGS&.include?(tag.name)
|
|
17
|
+
|
|
18
|
+
classes = tag.attributes["class"]&.value&.split(" ")
|
|
19
|
+
|
|
20
|
+
next if self.class::CLASSES.any? && (classes & self.class::CLASSES).blank?
|
|
21
|
+
|
|
22
|
+
generate_offense(self.class, processed_source, tag, message(tag))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
counter_correct?(processed_source)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
define_method "autocorrect" do |processed_source, offense|
|
|
29
|
+
return unless offense.context
|
|
30
|
+
|
|
31
|
+
lambda do |corrector|
|
|
32
|
+
if processed_source.file_content.include?("erblint:counter #{self.class.name.demodulize}")
|
|
33
|
+
# update the counter if exists
|
|
34
|
+
corrector.replace(offense.source_range, offense.context)
|
|
35
|
+
else
|
|
36
|
+
# add comment with counter if none
|
|
37
|
+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def message(_tag)
|
|
46
|
+
self.class::MESSAGE
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tags(processed_source)
|
|
50
|
+
processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def counter_correct?(processed_source)
|
|
54
|
+
comment_node = nil
|
|
55
|
+
expected_count = 0
|
|
56
|
+
rule_name = self.class.name.match(/:?:?(\w+)\Z/)[1]
|
|
57
|
+
offenses_count = @offenses.length
|
|
58
|
+
|
|
59
|
+
processed_source.parser.ast.descendants(:erb).each do |node|
|
|
60
|
+
indicator_node, _, code_node, = *node
|
|
61
|
+
indicator = indicator_node&.loc&.source
|
|
62
|
+
comment = code_node&.loc&.source&.strip
|
|
63
|
+
|
|
64
|
+
if indicator == "#" && comment.start_with?("erblint:count") && comment.match(rule_name)
|
|
65
|
+
comment_node = code_node
|
|
66
|
+
expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if offenses_count.zero?
|
|
71
|
+
add_offense(processed_source.to_source_range(comment_node.loc), "Unused erblint:count comment for #{rule_name}") if comment_node
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
first_offense = @offenses[0]
|
|
76
|
+
|
|
77
|
+
if comment_node.nil?
|
|
78
|
+
add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{offenses_count} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{offenses_count} %>")
|
|
79
|
+
else
|
|
80
|
+
clear_offenses
|
|
81
|
+
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
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
|
|
86
|
+
message ||= klass::MESSAGE
|
|
87
|
+
klass_name = klass.name.split("::")[-1]
|
|
88
|
+
offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
|
|
89
|
+
add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :coverage do
|
|
4
|
+
task :report do
|
|
5
|
+
require "simplecov"
|
|
6
|
+
require "simplecov-console"
|
|
7
|
+
|
|
8
|
+
SimpleCov.minimum_coverage 100
|
|
9
|
+
|
|
10
|
+
SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], "rails" do
|
|
11
|
+
formatter SimpleCov::Formatter::Console
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/tasks/docs.rake
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :docs do
|
|
4
|
+
task :livereload do
|
|
5
|
+
require "listen"
|
|
6
|
+
|
|
7
|
+
Rake::Task["docs:build"].execute
|
|
8
|
+
|
|
9
|
+
puts "Listening for changes to documentation..."
|
|
10
|
+
|
|
11
|
+
listener = Listen.to("app") do |modified, added, removed|
|
|
12
|
+
puts "modified absolute path: #{modified}"
|
|
13
|
+
puts "added absolute path: #{added}"
|
|
14
|
+
puts "removed absolute path: #{removed}"
|
|
15
|
+
|
|
16
|
+
Rake::Task["docs:build"].execute
|
|
17
|
+
end
|
|
18
|
+
listener.start # not blocking
|
|
19
|
+
sleep
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
task :build do
|
|
23
|
+
registry = generate_yard_registry
|
|
24
|
+
|
|
25
|
+
puts "Converting YARD documentation to Markdown files."
|
|
26
|
+
|
|
27
|
+
# Rails controller for rendering arbitrary ERB
|
|
28
|
+
view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context
|
|
29
|
+
components = [
|
|
30
|
+
Primer::Image,
|
|
31
|
+
Primer::LocalTime,
|
|
32
|
+
Primer::OcticonSymbolsComponent,
|
|
33
|
+
Primer::ImageCrop,
|
|
34
|
+
Primer::IconButton,
|
|
35
|
+
Primer::AutoComplete,
|
|
36
|
+
Primer::AutoComplete::Item,
|
|
37
|
+
Primer::AvatarComponent,
|
|
38
|
+
Primer::AvatarStackComponent,
|
|
39
|
+
Primer::BaseButton,
|
|
40
|
+
Primer::BlankslateComponent,
|
|
41
|
+
Primer::BorderBoxComponent,
|
|
42
|
+
Primer::BoxComponent,
|
|
43
|
+
Primer::BreadcrumbComponent,
|
|
44
|
+
Primer::ButtonComponent,
|
|
45
|
+
Primer::ButtonGroup,
|
|
46
|
+
Primer::Alpha::ButtonMarketing,
|
|
47
|
+
Primer::ClipboardCopy,
|
|
48
|
+
Primer::CloseButton,
|
|
49
|
+
Primer::CounterComponent,
|
|
50
|
+
Primer::DetailsComponent,
|
|
51
|
+
Primer::Dropdown,
|
|
52
|
+
Primer::DropdownMenuComponent,
|
|
53
|
+
Primer::FlashComponent,
|
|
54
|
+
Primer::FlexComponent,
|
|
55
|
+
Primer::FlexItemComponent,
|
|
56
|
+
Primer::HeadingComponent,
|
|
57
|
+
Primer::HiddenTextExpander,
|
|
58
|
+
Primer::LabelComponent,
|
|
59
|
+
Primer::LayoutComponent,
|
|
60
|
+
Primer::LinkComponent,
|
|
61
|
+
Primer::Markdown,
|
|
62
|
+
Primer::MenuComponent,
|
|
63
|
+
Primer::Navigation::TabComponent,
|
|
64
|
+
Primer::OcticonComponent,
|
|
65
|
+
Primer::PopoverComponent,
|
|
66
|
+
Primer::ProgressBarComponent,
|
|
67
|
+
Primer::StateComponent,
|
|
68
|
+
Primer::SpinnerComponent,
|
|
69
|
+
Primer::SubheadComponent,
|
|
70
|
+
Primer::TabContainerComponent,
|
|
71
|
+
Primer::TabNavComponent,
|
|
72
|
+
Primer::Beta::Text,
|
|
73
|
+
Primer::TimeAgoComponent,
|
|
74
|
+
Primer::TimelineItemComponent,
|
|
75
|
+
Primer::Tooltip,
|
|
76
|
+
Primer::Truncate,
|
|
77
|
+
Primer::UnderlineNavComponent
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
js_components = [
|
|
81
|
+
Primer::Dropdown,
|
|
82
|
+
Primer::LocalTime,
|
|
83
|
+
Primer::ImageCrop,
|
|
84
|
+
Primer::AutoComplete,
|
|
85
|
+
Primer::ClipboardCopy,
|
|
86
|
+
Primer::TabContainerComponent,
|
|
87
|
+
Primer::TabNavComponent,
|
|
88
|
+
Primer::TimeAgoComponent,
|
|
89
|
+
Primer::UnderlineNavComponent
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
all_components = Primer::Component.descendants - [Primer::BaseComponent]
|
|
93
|
+
components_needing_docs = all_components - components
|
|
94
|
+
|
|
95
|
+
args_for_components = []
|
|
96
|
+
classes_found_in_examples = []
|
|
97
|
+
|
|
98
|
+
errors = []
|
|
99
|
+
|
|
100
|
+
components.each do |component|
|
|
101
|
+
documentation = registry.get(component.name)
|
|
102
|
+
|
|
103
|
+
# Primer::AvatarComponent => Avatar
|
|
104
|
+
short_name = component.name.gsub(/Primer|::|Component/, "")
|
|
105
|
+
|
|
106
|
+
path = Pathname.new("docs/content/components/#{short_name.downcase}.md")
|
|
107
|
+
path.dirname.mkdir unless path.dirname.exist?
|
|
108
|
+
File.open(path, "w") do |f|
|
|
109
|
+
f.puts("---")
|
|
110
|
+
f.puts("title: #{short_name}")
|
|
111
|
+
f.puts("status: #{component.status.to_s.capitalize}")
|
|
112
|
+
f.puts("source: https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb")
|
|
113
|
+
f.puts("storybook: https://primer.style/view-components/stories/?path=/story/primer-#{short_name.underscore.dasherize}-component")
|
|
114
|
+
f.puts("---")
|
|
115
|
+
f.puts
|
|
116
|
+
f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'")
|
|
117
|
+
|
|
118
|
+
initialize_method = documentation.meths.find(&:constructor?)
|
|
119
|
+
|
|
120
|
+
if js_components.include?(component)
|
|
121
|
+
f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'")
|
|
122
|
+
f.puts
|
|
123
|
+
f.puts("<RequiresJSFlash />")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
f.puts
|
|
127
|
+
f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
|
|
128
|
+
f.puts
|
|
129
|
+
f.puts(view_context.render(inline: documentation.base_docstring))
|
|
130
|
+
|
|
131
|
+
if documentation.tags(:deprecated).any?
|
|
132
|
+
f.puts
|
|
133
|
+
f.puts("## Deprecation")
|
|
134
|
+
documentation.tags(:deprecated).each do |tag|
|
|
135
|
+
f.puts
|
|
136
|
+
f.puts view_context.render(inline: tag.text)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if documentation.tags(:accessibility).any?
|
|
141
|
+
f.puts
|
|
142
|
+
f.puts("## Accessibility")
|
|
143
|
+
documentation.tags(:accessibility).each do |tag|
|
|
144
|
+
f.puts
|
|
145
|
+
f.puts view_context.render(inline: tag.text)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
params = initialize_method.tags(:param)
|
|
150
|
+
|
|
151
|
+
errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
|
|
152
|
+
|
|
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"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
errors << { component.name => err }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
args = []
|
|
172
|
+
params.each do |tag|
|
|
173
|
+
default_value = pretty_default_value(tag, component)
|
|
174
|
+
|
|
175
|
+
args << {
|
|
176
|
+
"name" => tag.name,
|
|
177
|
+
"type" => tag.types.join(", "),
|
|
178
|
+
"default" => default_value,
|
|
179
|
+
"description" => view_context.render(inline: tag.text)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text)} |")
|
|
183
|
+
end
|
|
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
|
+
|
|
193
|
+
# Slots V2 docs
|
|
194
|
+
slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
|
|
195
|
+
|
|
196
|
+
if slot_v2_methods.any?
|
|
197
|
+
f.puts
|
|
198
|
+
f.puts("## Slots")
|
|
199
|
+
|
|
200
|
+
slot_v2_methods.each do |slot_documentation|
|
|
201
|
+
f.puts
|
|
202
|
+
f.puts("### `#{slot_documentation.name.to_s.capitalize}`")
|
|
203
|
+
|
|
204
|
+
if slot_documentation.base_docstring.present?
|
|
205
|
+
f.puts
|
|
206
|
+
f.puts(view_context.render(inline: slot_documentation.base_docstring))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
param_tags = slot_documentation.tags(:param)
|
|
210
|
+
if param_tags.any?
|
|
211
|
+
f.puts
|
|
212
|
+
f.puts("| Name | Type | Default | Description |")
|
|
213
|
+
f.puts("| :- | :- | :- | :- |")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
param_tags.each do |tag|
|
|
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
|
|
221
|
+
|
|
222
|
+
errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
|
|
223
|
+
|
|
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}"})
|
|
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("```")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
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
|
+
|
|
272
|
+
File.open("static/classes.yml", "w") do |f|
|
|
273
|
+
f.puts YAML.dump(classes_found_in_examples.sort.uniq)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
File.open("static/arguments.yml", "w") do |f|
|
|
277
|
+
f.puts YAML.dump(args_for_components)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Build system arguments docs from BaseComponent
|
|
281
|
+
documentation = registry.get(Primer::BaseComponent.name)
|
|
282
|
+
File.open("docs/content/system-arguments.md", "w") do |f|
|
|
283
|
+
f.puts("---")
|
|
284
|
+
f.puts("title: System arguments")
|
|
285
|
+
f.puts("---")
|
|
286
|
+
f.puts
|
|
287
|
+
f.puts("<!-- Warning: AUTO-GENERATED file, do not edit. Add code comments to your Ruby instead <3 -->")
|
|
288
|
+
f.puts
|
|
289
|
+
f.puts(documentation.base_docstring)
|
|
290
|
+
f.puts
|
|
291
|
+
|
|
292
|
+
initialize_method = documentation.meths.find(&:constructor?)
|
|
293
|
+
|
|
294
|
+
f.puts(view_context.render(inline: initialize_method.base_docstring))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
puts "Markdown compiled."
|
|
298
|
+
|
|
299
|
+
if components_needing_docs.any?
|
|
300
|
+
puts
|
|
301
|
+
puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(', ')}"
|
|
302
|
+
end
|
|
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
|
|
387
|
+
end
|