primer_view_components 0.0.44 → 0.0.48

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 (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