primer_view_components 0.0.44 → 0.0.48

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +187 -0
  3. data/app/components/primer/avatar_stack_component.rb +9 -3
  4. data/app/components/primer/base_component.rb +52 -23
  5. data/app/components/primer/beta/auto_complete.rb +159 -0
  6. data/app/components/primer/beta/auto_complete/auto_complete.d.ts +1 -0
  7. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.html.erb +0 -0
  8. data/app/components/primer/beta/auto_complete/auto_complete.js +1 -0
  9. data/app/components/primer/{auto_complete → beta/auto_complete}/auto_complete.ts +0 -0
  10. data/app/components/primer/beta/auto_complete/item.rb +44 -0
  11. data/app/components/primer/beta/avatar.rb +77 -0
  12. data/app/components/primer/border_box_component.rb +3 -0
  13. data/app/components/primer/clipboard_copy.rb +25 -7
  14. data/app/components/primer/component.rb +9 -1
  15. data/app/components/primer/details_component.rb +12 -8
  16. data/app/components/primer/image_crop.rb +1 -1
  17. data/app/components/primer/markdown.rb +9 -9
  18. data/app/components/primer/menu_component.rb +7 -3
  19. data/app/components/primer/navigation/tab_component.rb +19 -5
  20. data/app/components/primer/popover_component.rb +6 -3
  21. data/app/components/primer/primer.d.ts +1 -1
  22. data/app/components/primer/primer.js +1 -1
  23. data/app/components/primer/primer.ts +1 -1
  24. data/app/components/primer/tab_nav_component.rb +8 -6
  25. data/app/components/primer/timeline_item_component.rb +2 -2
  26. data/app/components/primer/tooltip.rb +1 -1
  27. data/app/components/primer/truncate.rb +5 -0
  28. data/app/components/primer/underline_nav_component.rb +12 -6
  29. data/{app/lib → lib}/primer/classify.rb +16 -33
  30. data/{app/lib → lib}/primer/classify/cache.rb +6 -40
  31. data/{app/lib → lib}/primer/classify/flex.rb +0 -0
  32. data/{app/lib → lib}/primer/classify/functional_background_colors.rb +2 -0
  33. data/{app/lib → lib}/primer/classify/functional_border_colors.rb +2 -0
  34. data/{app/lib → lib}/primer/classify/functional_colors.rb +0 -0
  35. data/{app/lib → lib}/primer/classify/functional_text_colors.rb +2 -0
  36. data/{app/lib → lib}/primer/classify/grid.rb +0 -0
  37. data/lib/primer/classify/utilities.rb +148 -0
  38. data/lib/primer/classify/utilities.yml +1271 -0
  39. data/lib/primer/view_components.rb +1 -0
  40. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +5 -4
  41. data/lib/primer/view_components/linters/button_component_migration_counter.rb +9 -5
  42. data/lib/primer/view_components/linters/helpers.rb +132 -17
  43. data/lib/primer/view_components/statuses.rb +14 -0
  44. data/lib/primer/view_components/version.rb +1 -1
  45. data/lib/rubocop/config/default.yml +12 -0
  46. data/lib/rubocop/cop/primer.rb +4 -0
  47. data/lib/rubocop/cop/primer/no_tag_memoize.rb +42 -0
  48. data/lib/rubocop/cop/primer/system_argument_instead_of_class.rb +75 -0
  49. data/lib/tasks/docs.rake +72 -18
  50. data/lib/tasks/utilities.rake +105 -0
  51. data/lib/yard/docs_helper.rb +1 -1
  52. data/static/statuses.json +4 -4
  53. metadata +30 -21
  54. data/app/components/primer/auto_complete.rb +0 -156
  55. data/app/components/primer/auto_complete/item.rb +0 -42
  56. data/app/components/primer/avatar_component.rb +0 -75
  57. 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
 
@@ -7,7 +7,7 @@ module ERBLint
7
7
  module ArgumentMappers
8
8
  # Maps element attributes to system arguments.
9
9
  class SystemArguments
10
- STRING_PARAETERS = %w[aria- data-].freeze
10
+ STRING_PARAMETERS = %w[aria- data-].freeze
11
11
  TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
12
12
 
13
13
  attr_reader :attribute
@@ -29,9 +29,10 @@ module ERBLint
29
29
  { test_selector: m[:selector].tr("'", '"') }
30
30
  elsif attr_name == "data-test-selector"
31
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 }
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 }
35
36
  else
36
37
  raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
37
38
  end
@@ -21,14 +21,18 @@ module ERBLint
21
21
  nil
22
22
  end
23
23
 
24
- def message(tag)
25
- args = map_arguments(tag)
24
+ def correction(args)
25
+ return nil if args.nil?
26
26
 
27
+ correction = "<%= render Primer::ButtonComponent.new"
28
+ correction += "(#{args})" if args.present?
29
+ "#{correction} do %>"
30
+ end
31
+
32
+ def message(args)
27
33
  return MESSAGE if args.nil?
28
34
 
29
- msg = "#{MESSAGE}\n\nTry using:\n\n<%= render Primer::ButtonComponent.new"
30
- msg += "(#{args})" if args.present?
31
- "#{msg} %>\n\nInstead of:\n"
35
+ "#{MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
32
36
  end
33
37
  end
34
38
  end
@@ -7,34 +7,69 @@ module ERBLint
7
7
  module Linters
8
8
  # Helper methods for linting ERB.
9
9
  module Helpers
10
+ # from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
11
+ SELF_CLOSING_TAGS = %w[
12
+ area base br col command embed hr input keygen
13
+ link menuitem meta param source track wbr img
14
+ ].freeze
15
+
16
+ DUMP_FILE = ".erblint-counter-ignore.json"
17
+
10
18
  def self.included(base)
11
19
  base.include(ERBLint::LinterRegistry)
12
20
 
13
21
  define_method "run" do |processed_source|
14
- tags(processed_source).each do |tag|
22
+ @total_offenses = 0
23
+ @offenses_not_corrected = 0
24
+ tags = tags(processed_source)
25
+ tag_tree = build_tag_tree(tags)
26
+
27
+ tags.each do |tag|
15
28
  next if tag.closing?
16
29
  next unless self.class::TAGS&.include?(tag.name)
17
30
 
18
- classes = tag.attributes["class"]&.value&.split(" ")
31
+ classes = tag.attributes["class"]&.value&.split(" ") || []
19
32
 
20
- next if self.class::CLASSES.any? && (classes & self.class::CLASSES).blank?
33
+ tag_tree[tag][:offense] = false
21
34
 
22
- generate_offense(self.class, processed_source, tag, message(tag))
35
+ next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
36
+
37
+ args = map_arguments(tag)
38
+ correction = correction(args)
39
+
40
+ tag_tree[tag][:offense] = true
41
+ tag_tree[tag][:correctable] = !correction.nil?
42
+ tag_tree[tag][:message] = message(args)
43
+ tag_tree[tag][:correction] = correction
44
+ end
45
+
46
+ tag_tree.each do |tag, h|
47
+ next unless h[:offense]
48
+
49
+ @total_offenses += 1
50
+ # We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
51
+ if h[:correctable]
52
+ add_offense(tag.loc, h[:message], h[:correction])
53
+ add_offense(h[:closing].loc, h[:message], "<% end %>")
54
+ else
55
+ @offenses_not_corrected += 1
56
+ generate_offense(self.class, processed_source, tag, h[:message])
57
+ end
23
58
  end
24
59
 
25
60
  counter_correct?(processed_source)
61
+
62
+ dump_data(processed_source) if ENV["DUMP_LINT_DATA"] == "1"
26
63
  end
27
64
 
28
65
  define_method "autocorrect" do |processed_source, offense|
29
66
  return unless offense.context
30
67
 
31
68
  lambda do |corrector|
32
- if 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)
69
+ if offense.context.include?(counter_disable)
70
+ correct_counter(corrector, processed_source, offense)
35
71
  else
36
- # add comment with counter if none
37
- corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
72
+ corrector.replace(offense.source_range, offense.context)
38
73
  end
39
74
  end
40
75
  end
@@ -42,10 +77,77 @@ module ERBLint
42
77
 
43
78
  private
44
79
 
80
+ # Override this function to convert the HTML element attributes to argument for a component.
81
+ #
82
+ # @return [Hash] if possible to map all attributes to arguments.
83
+ # @return [Nil] if cannot map to arguments.
84
+ def map_arguments(_tag)
85
+ nil
86
+ end
87
+
88
+ # Override this function to define how to autocorrect an element to a component.
89
+ #
90
+ # @return [String] with the text to replace the HTML element if possible to correct.
91
+ # @return [Nil] if cannot correct element.
92
+ def correction(_tag)
93
+ nil
94
+ end
95
+
96
+ # Override this function to customize the linter message.
97
+ #
98
+ # @return [String] message to show on linter error.
45
99
  def message(_tag)
46
100
  self.class::MESSAGE
47
101
  end
48
102
 
103
+ def counter_disable
104
+ "erblint:counter #{self.class.name.demodulize}"
105
+ end
106
+
107
+ def correct_counter(corrector, processed_source, offense)
108
+ if processed_source.file_content.include?(counter_disable)
109
+ # update the counter if exists
110
+ corrector.replace(offense.source_range, offense.context)
111
+ else
112
+ # add comment with counter if none
113
+ corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
114
+ end
115
+ end
116
+
117
+ # This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
118
+ # From the tags, we build a structured tree which represents the tag hierarchy.
119
+ # With this, we are able to know where the tags start and end.
120
+ def build_tag_tree(tags)
121
+ tag_tree = {}
122
+ current_opened_tag = nil
123
+
124
+ tags.each do |tag|
125
+ if tag.closing?
126
+ if current_opened_tag && tag.name == current_opened_tag.name
127
+ tag_tree[current_opened_tag][:closing] = tag
128
+ current_opened_tag = tag_tree[current_opened_tag][:parent]
129
+ end
130
+
131
+ next
132
+ end
133
+
134
+ self_closing = self_closing?(tag)
135
+
136
+ tag_tree[tag] = {
137
+ closing: self_closing ? tag : nil,
138
+ parent: current_opened_tag
139
+ }
140
+
141
+ current_opened_tag = tag unless self_closing
142
+ end
143
+
144
+ tag_tree
145
+ end
146
+
147
+ def self_closing?(tag)
148
+ tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
149
+ end
150
+
49
151
  def tags(processed_source)
50
152
  processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
51
153
  end
@@ -54,7 +156,6 @@ module ERBLint
54
156
  comment_node = nil
55
157
  expected_count = 0
56
158
  rule_name = self.class.name.match(/:?:?(\w+)\Z/)[1]
57
- offenses_count = @offenses.length
58
159
 
59
160
  processed_source.parser.ast.descendants(:erb).each do |node|
60
161
  indicator_node, _, code_node, = *node
@@ -62,32 +163,46 @@ module ERBLint
62
163
  comment = code_node&.loc&.source&.strip
63
164
 
64
165
  if indicator == "#" && comment.start_with?("erblint:count") && comment.match(rule_name)
65
- comment_node = code_node
166
+ comment_node = node
66
167
  expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
67
168
  end
68
169
  end
69
170
 
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
171
+ if @offenses_not_corrected.zero?
172
+ # have to adjust to get `\n` so we delete the whole line
173
+ add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:count comment for #{rule_name}", "") if comment_node
72
174
  return
73
175
  end
74
176
 
75
177
  first_offense = @offenses[0]
76
178
 
77
179
  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
180
+ add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{@offenses_not_corrected} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
181
+ elsif expected_count != @offenses_not_corrected
182
+ add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{@offenses_not_corrected}.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
183
+ # the only offenses remaining are not autocorrectable, so we can ignore them
184
+ elsif expected_count == @offenses_not_corrected && @offenses.size == @offenses_not_corrected
80
185
  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
186
  end
83
187
  end
84
188
 
85
189
  def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
86
190
  message ||= klass::MESSAGE
87
- klass_name = klass.name.split("::")[-1]
191
+ klass_name = klass.name.demodulize
88
192
  offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
89
193
  add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
90
194
  end
195
+
196
+ def dump_data(processed_source)
197
+ return if @total_offenses.zero?
198
+
199
+ data = File.exist?(DUMP_FILE) ? JSON.parse(File.read(DUMP_FILE)) : {}
200
+
201
+ data[processed_source.filename] ||= {}
202
+ data[processed_source.filename][self.class.name.demodulize] = @total_offenses
203
+
204
+ File.write(DUMP_FILE, JSON.pretty_generate(data))
205
+ end
91
206
  end
92
207
  end
93
208
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Primer
6
+ # :nodoc:
7
+ module ViewComponents
8
+ STATUSES = JSON.parse(
9
+ File.read(
10
+ File.join(File.dirname(__FILE__), "../../../static/statuses.json")
11
+ )
12
+ ).freeze
13
+ end
14
+ end
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 44
8
+ PATCH = 48
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -0,0 +1,12 @@
1
+ require:
2
+ - rubocop/cop/primer
3
+
4
+ AllCops:
5
+ DisabledByDefault: true
6
+
7
+ Primer/SystemArgumentInsteadOfClass:
8
+ Enabled: true
9
+
10
+ Primer/NoTagMemoize:
11
+ Enabled: false
12
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop/cop/primer/no_tag_memoize"
4
+ require "rubocop/cop/primer/system_argument_instead_of_class"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Primer
8
+ # This cop ensures that tags are not set with ||=
9
+ #
10
+ # bad
11
+ # @system_arguments[:tag] ||= :h1
12
+ #
13
+ # good
14
+ # @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, DEFAULT_TAG)
15
+ #
16
+ # good
17
+ # @system_arguments[:tag] = :h2
18
+ class NoTagMemoize < RuboCop::Cop::Cop
19
+ INVALID_MESSAGE = <<~STR
20
+ Avoid `[:tag] ||=`. Instead, try one of the following:
21
+ - Don't allow consumers to update the tag by having a fixed tag (e.g. `system_arguments[:tag] = :div`)
22
+ - Use the `fetch_or_fallback` helper to only allow a tag from a restricted list.
23
+ STR
24
+
25
+ def_node_search :tag_memoized?, <<~PATTERN
26
+ (or-asgn
27
+ (send
28
+ _
29
+ _
30
+ (sym :tag)
31
+ )
32
+ _
33
+ )
34
+ PATTERN
35
+
36
+ def on_or_asgn(node)
37
+ add_offense(node, message: INVALID_MESSAGE) if tag_memoized?(node)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "primer/classify/utilities"
5
+ require "primer/view_components/statuses"
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Primer
10
+ # This cop ensures that components use System Arguments instead of CSS classes.
11
+ #
12
+ # bad
13
+ # Component.new(classes: "mr-1")
14
+ #
15
+ # good
16
+ # Component.new(mr: 1)
17
+ class SystemArgumentInsteadOfClass < RuboCop::Cop::Cop
18
+ INVALID_MESSAGE = <<~STR
19
+ Avoid using CSS classes when you can use System Arguments: https://primer.style/view-components/system-arguments.
20
+ STR
21
+
22
+ def on_send(node)
23
+ return unless node.method_name == :new
24
+ return unless ::Primer::ViewComponents::STATUSES.key?(node.receiver.const_name)
25
+ return unless node.arguments?
26
+
27
+ # we are looking for hash arguments and they are always last
28
+ kwargs = node.arguments.last
29
+
30
+ return unless kwargs.type == :hash
31
+
32
+ # find classes pair
33
+ classes_arg = kwargs.pairs.find { |kwarg| kwarg.key.value == :classes }
34
+
35
+ return if classes_arg.nil?
36
+ return unless classes_arg.value.type == :str
37
+
38
+ # get actual classes
39
+ classes = classes_arg.value.value
40
+
41
+ system_arguments = ::Primer::Classify::Utilities.classes_to_hash(classes)
42
+
43
+ # no classes are fixable
44
+ return if system_arguments[:classes] == classes
45
+
46
+ add_offense(classes_arg, message: INVALID_MESSAGE)
47
+ end
48
+
49
+ def autocorrect(node)
50
+ lambda do |corrector|
51
+ system_arguments = ::Primer::Classify::Utilities.classes_to_hash(node.value.value)
52
+ corrector.replace(node.loc.expression, arguments_as_string(system_arguments))
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def arguments_as_string(system_arguments)
59
+ system_arguments.map do |key, value|
60
+ val = case value
61
+ when Symbol
62
+ ":#{value}"
63
+ when String
64
+ value.to_json
65
+ else
66
+ value
67
+ end
68
+
69
+ "#{key}: #{val}"
70
+ end.join(", ")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/tasks/docs.rake CHANGED
@@ -32,9 +32,9 @@ namespace :docs do
32
32
  Primer::OcticonSymbolsComponent,
33
33
  Primer::ImageCrop,
34
34
  Primer::IconButton,
35
- Primer::AutoComplete,
36
- Primer::AutoComplete::Item,
37
- Primer::AvatarComponent,
35
+ Primer::Beta::AutoComplete,
36
+ Primer::Beta::AutoComplete::Item,
37
+ Primer::Beta::Avatar,
38
38
  Primer::AvatarStackComponent,
39
39
  Primer::BaseButton,
40
40
  Primer::BlankslateComponent,
@@ -81,7 +81,7 @@ namespace :docs do
81
81
  Primer::Dropdown,
82
82
  Primer::LocalTime,
83
83
  Primer::ImageCrop,
84
- Primer::AutoComplete,
84
+ Primer::Beta::AutoComplete,
85
85
  Primer::ClipboardCopy,
86
86
  Primer::TabContainerComponent,
87
87
  Primer::TabNavComponent,
@@ -97,28 +97,30 @@ namespace :docs do
97
97
 
98
98
  errors = []
99
99
 
100
- components.each do |component|
100
+ # Deletes docs before regenerating them, guaranteeing that we don't keep stale docs.
101
+ FileUtils.rm_rf(Dir.glob("docs/content/components/**/*.md"))
102
+
103
+ components.sort_by(&:name).each do |component|
101
104
  documentation = registry.get(component.name)
102
105
 
103
- # Primer::AvatarComponent => Avatar
104
- short_name = component.name.gsub(/Primer|::|Component/, "")
106
+ data = docs_metadata(component)
105
107
 
106
- path = Pathname.new("docs/content/components/#{short_name.downcase}.md")
108
+ path = Pathname.new(data[:path])
107
109
  path.dirname.mkdir unless path.dirname.exist?
108
110
  File.open(path, "w") do |f|
109
111
  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")
112
+ f.puts("title: #{data[:title]}")
113
+ f.puts("status: #{data[:status]}")
114
+ f.puts("source: #{data[:source]}")
115
+ f.puts("storybook: #{data[:storybook]}")
114
116
  f.puts("---")
115
117
  f.puts
116
- f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'")
118
+ f.puts("import Example from '#{data[:example_path]}'")
117
119
 
118
120
  initialize_method = documentation.meths.find(&:constructor?)
119
121
 
120
122
  if js_components.include?(component)
121
- f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'")
123
+ f.puts("import RequiresJSFlash from '#{data[:require_js_path]}'")
122
124
  f.puts
123
125
  f.puts("<RequiresJSFlash />")
124
126
  end
@@ -176,15 +178,15 @@ namespace :docs do
176
178
  "name" => tag.name,
177
179
  "type" => tag.types.join(", "),
178
180
  "default" => default_value,
179
- "description" => view_context.render(inline: tag.text)
181
+ "description" => view_context.render(inline: tag.text.squish)
180
182
  }
181
183
 
182
- f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text)} |")
184
+ f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text.squish)} |")
183
185
  end
184
186
 
185
187
  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
+ "component" => data[:title],
189
+ "source" => data[:source],
188
190
  "parameters" => args
189
191
  }
190
192
 
@@ -346,6 +348,7 @@ namespace :docs do
346
348
  end
347
349
 
348
350
  def generate_yard_registry
351
+ ENV["SKIP_STORYBOOK_PRELOAD"] = "1"
349
352
  require File.expand_path("./../../demo/config/environment.rb", __dir__)
350
353
  require "primer/view_components"
351
354
  require "yard/docs_helper"
@@ -384,4 +387,55 @@ namespace :docs do
384
387
 
385
388
  pretty_value(constant_value)
386
389
  end
390
+
391
+ def status_module_and_short_name(component)
392
+ name_with_status = component.name.gsub(/Primer::|Component/, "")
393
+
394
+ m = name_with_status.match(/(?<status>Beta|Alpha|Deprecated)?(::)?(?<name>.*)/)
395
+ [m[:status]&.downcase, m[:name].gsub("::", "")]
396
+ end
397
+
398
+ def docs_metadata(component)
399
+ (status_module, short_name) = status_module_and_short_name(component)
400
+ status_path = status_module.nil? ? "" : "#{status_module}/"
401
+ status = component.status.to_s
402
+
403
+ {
404
+ title: short_name,
405
+ status: status.capitalize,
406
+ source: source_url(component),
407
+ storybook: storybook_url(component),
408
+ path: "docs/content/components/#{status_path}#{short_name.downcase}.md",
409
+ example_path: example_path(component),
410
+ require_js_path: require_js_path(component)
411
+ }
412
+ end
413
+
414
+ def source_url(component)
415
+ path = component.name.split("::").map(&:underscore).join("/")
416
+
417
+ "https://github.com/primer/view_components/tree/main/app/components/#{path}.rb"
418
+ end
419
+
420
+ def storybook_url(component)
421
+ path = component.name.split("::").map { |n| n.underscore.dasherize }.join("-")
422
+
423
+ "https://primer.style/view-components/stories/?path=/story/#{path}"
424
+ end
425
+
426
+ def example_path(component)
427
+ example_path = "../../src/@primer/gatsby-theme-doctocat/components/example"
428
+ example_path = "../#{example_path}" if status_module?(component)
429
+ example_path
430
+ end
431
+
432
+ def require_js_path(component)
433
+ require_js_path = "../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash"
434
+ require_js_path = "../#{require_js_path}" if status_module?(component)
435
+ require_js_path
436
+ end
437
+
438
+ def status_module?(component)
439
+ (%w[Alpha Beta] & component.name.split("::")).any?
440
+ end
387
441
  end