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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +265 -1
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/components/primer/alpha/button_marketing.rb +70 -0
  6. data/app/components/primer/auto_complete.rb +99 -42
  7. data/app/components/primer/auto_complete/auto_complete.html.erb +1 -0
  8. data/app/components/primer/avatar_stack_component.rb +7 -1
  9. data/app/components/primer/base_component.rb +62 -26
  10. data/app/components/primer/beta/text.rb +27 -0
  11. data/app/components/primer/blankslate_component.html.erb +1 -0
  12. data/app/components/primer/blankslate_component.rb +64 -45
  13. data/app/components/primer/border_box_component.rb +3 -0
  14. data/app/components/primer/button_component.rb +3 -2
  15. data/app/components/primer/button_group.rb +1 -1
  16. data/app/components/primer/clipboard_copy.rb +25 -7
  17. data/app/components/primer/component.rb +5 -1
  18. data/app/components/primer/details_component.rb +18 -3
  19. data/app/components/primer/dropdown.d.ts +1 -0
  20. data/app/components/primer/{dropdown_component.html.erb → dropdown.html.erb} +2 -1
  21. data/app/components/primer/dropdown.js +1 -0
  22. data/app/components/primer/dropdown.rb +149 -0
  23. data/app/components/primer/dropdown.ts +1 -0
  24. data/app/components/primer/dropdown/menu.d.ts +1 -0
  25. data/app/components/primer/dropdown/menu.html.erb +25 -0
  26. data/app/components/primer/dropdown/menu.js +1 -0
  27. data/app/components/primer/dropdown/menu.rb +99 -0
  28. data/app/components/primer/dropdown/menu.ts +1 -0
  29. data/app/components/primer/heading_component.rb +1 -1
  30. data/app/components/primer/hidden_text_expander.rb +2 -2
  31. data/app/components/primer/icon_button.rb +1 -1
  32. data/app/components/primer/image_crop.rb +2 -2
  33. data/app/components/primer/markdown.rb +6 -2
  34. data/app/components/primer/menu_component.rb +7 -3
  35. data/app/components/primer/navigation/tab_component.rb +6 -6
  36. data/app/components/primer/octicon_component.rb +4 -3
  37. data/app/components/primer/popover_component.rb +2 -2
  38. data/app/components/primer/primer.d.ts +1 -0
  39. data/app/components/primer/primer.js +1 -0
  40. data/app/components/primer/primer.ts +1 -0
  41. data/app/components/primer/spinner_component.rb +2 -0
  42. data/app/components/primer/tab_nav_component.html.erb +4 -2
  43. data/app/components/primer/tab_nav_component.rb +48 -6
  44. data/app/components/primer/tooltip.rb +1 -1
  45. data/app/components/primer/truncate.rb +6 -2
  46. data/app/components/primer/underline_nav_component.html.erb +1 -1
  47. data/app/components/primer/underline_nav_component.rb +27 -5
  48. data/app/lib/primer/tabbed_component_helper.rb +2 -2
  49. data/{app/lib → lib}/primer/classify.rb +41 -35
  50. data/{app/lib → lib}/primer/classify/cache.rb +16 -35
  51. data/{app/lib → lib}/primer/classify/flex.rb +0 -0
  52. data/{app/lib → lib}/primer/classify/functional_background_colors.rb +2 -0
  53. data/{app/lib → lib}/primer/classify/functional_border_colors.rb +2 -0
  54. data/{app/lib → lib}/primer/classify/functional_colors.rb +0 -0
  55. data/{app/lib → lib}/primer/classify/functional_text_colors.rb +2 -0
  56. data/lib/primer/classify/grid.rb +45 -0
  57. data/lib/primer/classify/utilities.rb +137 -0
  58. data/lib/primer/classify/utilities.yml +1271 -0
  59. data/lib/primer/view_components.rb +1 -0
  60. data/lib/primer/view_components/engine.rb +2 -0
  61. data/lib/primer/view_components/linters.rb +3 -0
  62. data/lib/primer/view_components/linters/argument_mappers/button.rb +82 -0
  63. data/lib/primer/view_components/linters/argument_mappers/conversion_error.rb +10 -0
  64. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +47 -0
  65. data/lib/primer/view_components/linters/button_component_migration_counter.rb +39 -0
  66. data/lib/primer/view_components/linters/flash_component_migration_counter.rb +16 -0
  67. data/lib/primer/view_components/linters/helpers.rb +191 -0
  68. data/lib/primer/view_components/version.rb +1 -1
  69. data/lib/tasks/docs.rake +180 -108
  70. data/lib/tasks/utilities.rake +105 -0
  71. data/lib/yard/docs_helper.rb +12 -2
  72. data/static/statuses.json +7 -5
  73. metadata +50 -20
  74. data/app/components/primer/button_marketing_component.rb +0 -68
  75. data/app/components/primer/dropdown/menu_component.html.erb +0 -12
  76. data/app/components/primer/dropdown/menu_component.rb +0 -46
  77. data/app/components/primer/dropdown_component.rb +0 -73
  78. data/app/components/primer/text_component.rb +0 -25
  79. data/app/lib/primer/classify/spacing.rb +0 -63
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "primer/classify"
3
4
  require "primer/view_components/version"
4
5
  require "primer/view_components/engine"
5
6
 
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "linters", "*.rb")].sort.each { |file| require file }
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ module ArgumentMappers
6
+ # Error when converting arguments.
7
+ class ConversionError < StandardError; end
8
+ end
9
+ end
10
+ 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
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 41
8
+ PATCH = 46
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  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
- require File.expand_path("./../../demo/config/environment.rb", __dir__)
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::ButtonMarketingComponent,
46
+ Primer::Alpha::ButtonMarketing,
67
47
  Primer::ClipboardCopy,
68
48
  Primer::CloseButton,
69
49
  Primer::CounterComponent,
70
50
  Primer::DetailsComponent,
71
- Primer::DropdownComponent,
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::TextComponent,
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(:accessibility).any?
131
+ if documentation.tags(:deprecated).any?
148
132
  f.puts
149
- f.puts("## Accessibility")
150
- documentation.tags(:accessibility).each do |tag|
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(:deprecated).any?
140
+ if documentation.tags(:accessibility).any?
157
141
  f.puts
158
- f.puts("## Deprecation")
159
- documentation.tags(:deprecated).each do |tag|
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
- initialize_method = documentation.meths.find(&:constructor?)
149
+ params = initialize_method.tags(:param)
166
150
 
167
- if initialize_method.tags(:example).any?
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
- initialize_method.tags(:example).each do |tag|
175
- (name, description) = tag.name.split("|")
176
- f.puts
177
- f.puts("### #{name}")
178
- if description
179
- f.puts
180
- f.puts(description)
181
- end
182
- f.puts
183
- html = view_context.render(inline: tag.text)
184
- html.scan(/class="([^"]*)"/) do |classnames|
185
- classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
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
- f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
188
- f.puts
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
- params = initialize_method.tags(:param)
195
- if params.any?
196
- f.puts
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
- component_args = {
230
- "component" => short_name,
231
- "source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb",
232
- "parameters" => args
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
- args_for_components << component_args
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
- params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
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
- default =
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
- f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |")
272
- end
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