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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +269 -3
  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 -41
  7. data/app/components/primer/auto_complete/auto_complete.html.erb +1 -0
  8. data/app/components/primer/avatar_stack_component.rb +7 -4
  9. data/app/components/primer/base_component.rb +17 -7
  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 +68 -49
  13. data/app/components/primer/button_component.rb +3 -2
  14. data/app/components/primer/button_group.rb +2 -2
  15. data/app/components/primer/clipboard_copy_component.js +13 -2
  16. data/app/components/primer/clipboard_copy_component.ts +15 -2
  17. data/app/components/primer/component.rb +6 -1
  18. data/app/components/primer/counter_component.rb +6 -1
  19. data/app/components/primer/details_component.rb +12 -1
  20. data/app/components/primer/dropdown.d.ts +1 -0
  21. data/app/components/primer/{dropdown_component.html.erb → dropdown.html.erb} +2 -1
  22. data/app/components/primer/dropdown.js +1 -0
  23. data/app/components/primer/dropdown.rb +149 -0
  24. data/app/components/primer/dropdown.ts +1 -0
  25. data/app/components/primer/dropdown/menu.d.ts +1 -0
  26. data/app/components/primer/dropdown/menu.html.erb +25 -0
  27. data/app/components/primer/dropdown/menu.js +1 -0
  28. data/app/components/primer/dropdown/menu.rb +99 -0
  29. data/app/components/primer/dropdown/menu.ts +1 -0
  30. data/app/components/primer/flash_component.rb +2 -2
  31. data/app/components/primer/flex_component.rb +27 -0
  32. data/app/components/primer/flex_item_component.rb +1 -1
  33. data/app/components/primer/heading_component.rb +11 -18
  34. data/app/components/primer/hidden_text_expander.rb +3 -3
  35. data/app/components/primer/icon_button.rb +20 -3
  36. data/app/components/primer/image.rb +46 -0
  37. data/app/components/primer/image_crop.d.ts +1 -0
  38. data/app/components/primer/image_crop.html.erb +12 -0
  39. data/app/components/primer/image_crop.js +1 -0
  40. data/app/components/primer/image_crop.rb +36 -0
  41. data/app/components/primer/image_crop.ts +1 -0
  42. data/app/components/primer/label_component.rb +6 -2
  43. data/app/components/primer/local_time.d.ts +1 -0
  44. data/app/components/primer/local_time.js +1 -0
  45. data/app/components/primer/local_time.rb +59 -0
  46. data/app/components/primer/local_time.ts +1 -0
  47. data/app/components/primer/{markdown_component.rb → markdown.rb} +11 -6
  48. data/app/components/primer/navigation/tab_component.rb +10 -3
  49. data/app/components/primer/octicon_component.html.erb +7 -0
  50. data/app/components/primer/octicon_component.rb +25 -15
  51. data/app/components/primer/octicon_symbols_component.html.erb +3 -0
  52. data/app/components/primer/octicon_symbols_component.rb +61 -0
  53. data/app/components/primer/primer.d.ts +3 -0
  54. data/app/components/primer/primer.js +3 -0
  55. data/app/components/primer/primer.ts +3 -0
  56. data/app/components/primer/spinner_component.rb +4 -2
  57. data/app/components/primer/subhead_component.rb +34 -4
  58. data/app/components/primer/tab_nav_component.html.erb +5 -1
  59. data/app/components/primer/tab_nav_component.rb +62 -9
  60. data/app/components/primer/{tooltip_component.rb → tooltip.rb} +10 -8
  61. data/app/components/primer/truncate.rb +6 -2
  62. data/app/components/primer/underline_nav_component.html.erb +1 -1
  63. data/app/components/primer/underline_nav_component.rb +17 -1
  64. data/app/lib/primer/classify.rb +21 -8
  65. data/app/lib/primer/classify/cache.rb +16 -1
  66. data/app/lib/primer/classify/grid.rb +45 -0
  67. data/app/lib/primer/octicon/cache.rb +4 -0
  68. data/app/lib/primer/tabbed_component_helper.rb +2 -2
  69. data/app/lib/primer/view_helper.rb +2 -1
  70. data/lib/primer/view_components.rb +1 -1
  71. data/lib/primer/view_components/engine.rb +2 -0
  72. data/lib/primer/view_components/linters.rb +3 -0
  73. data/lib/primer/view_components/linters/argument_mappers/button.rb +82 -0
  74. data/lib/primer/view_components/linters/argument_mappers/conversion_error.rb +10 -0
  75. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +46 -0
  76. data/lib/primer/view_components/linters/button_component_migration_counter.rb +35 -0
  77. data/lib/primer/view_components/linters/flash_component_migration_counter.rb +16 -0
  78. data/lib/primer/view_components/linters/helpers.rb +93 -0
  79. data/lib/primer/view_components/version.rb +1 -1
  80. data/lib/tasks/coverage.rake +14 -0
  81. data/lib/tasks/docs.rake +387 -0
  82. data/lib/tasks/statuses.rake +12 -0
  83. data/lib/yard/docs_helper.rb +67 -0
  84. data/static/statuses.json +56 -1
  85. metadata +72 -13
  86. data/app/components/primer/button_marketing_component.rb +0 -68
  87. data/app/components/primer/dropdown/menu_component.html.erb +0 -12
  88. data/app/components/primer/dropdown/menu_component.rb +0 -46
  89. data/app/components/primer/dropdown_component.rb +0 -73
  90. 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|
@@ -27,7 +27,7 @@ module Primer
27
27
  statuses = generate_statuses
28
28
 
29
29
  File.open(File.join(path, DEFAULT_STATUS_FILE_NAME), "w") do |f|
30
- f.write(statuses.to_json)
30
+ f.write(JSON.pretty_generate(statuses))
31
31
  f.write($INPUT_RECORD_SEPARATOR)
32
32
  end
33
33
  end
@@ -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,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
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 39
8
+ PATCH = 44
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  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
@@ -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