# frozen_string_literal: true namespace :docs do task :livereload do require "listen" Rake::Task["docs:build"].execute puts "Listening for changes to documentation..." listener = Listen.to("app") do |modified, added, removed| puts "modified absolute path: #{modified}" puts "added absolute path: #{added}" puts "removed absolute path: #{removed}" Rake::Task["docs:build"].execute end listener.start # not blocking sleep end task :build do require File.expand_path("./../../demo/config/environment.rb", __dir__) require "primer/view_components" require "yard/docs_helper" require "view_component/test_helpers" include ViewComponent::TestHelpers include Primer::ViewHelper include YARD::DocsHelper Dir["./app/components/primer/**/*.rb"].sort.each { |file| require file } YARD::Rake::YardocTask.new # Custom tags for yard YARD::Tags::Library.define_tag("Accessibility", :accessibility) YARD::Tags::Library.define_tag("Deprecation", :deprecation) puts "Building YARD documentation." Rake::Task["yard"].execute puts "Converting YARD documentation to Markdown files." # Rails controller for rendering arbitrary ERB view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context registry = YARD::RegistryStore.new registry.load!(".yardoc") components = [ Primer::OcticonSymbolsComponent, Primer::ImageCrop, Primer::IconButton, Primer::AutoComplete, Primer::AutoComplete::Item, Primer::AvatarComponent, Primer::AvatarStackComponent, Primer::BaseButton, Primer::BlankslateComponent, Primer::BorderBoxComponent, Primer::BoxComponent, Primer::BreadcrumbComponent, Primer::ButtonComponent, Primer::ButtonGroup, Primer::ButtonMarketingComponent, Primer::ClipboardCopy, Primer::CloseButton, Primer::CounterComponent, Primer::DetailsComponent, Primer::DropdownComponent, Primer::DropdownMenuComponent, Primer::FlashComponent, Primer::FlexComponent, Primer::FlexItemComponent, Primer::HeadingComponent, Primer::HiddenTextExpander, Primer::LabelComponent, Primer::LayoutComponent, Primer::LinkComponent, Primer::Markdown, Primer::MenuComponent, Primer::Navigation::TabComponent, Primer::OcticonComponent, Primer::PopoverComponent, Primer::ProgressBarComponent, Primer::StateComponent, Primer::SpinnerComponent, Primer::SubheadComponent, Primer::TabContainerComponent, Primer::TabNavComponent, Primer::TextComponent, Primer::TimeAgoComponent, Primer::TimelineItemComponent, Primer::TooltipComponent, Primer::Truncate, Primer::UnderlineNavComponent ] js_components = [ Primer::ImageCrop, Primer::AutoComplete, Primer::ClipboardCopy, Primer::TabContainerComponent, Primer::TabNavComponent, Primer::TimeAgoComponent, Primer::UnderlineNavComponent ] all_components = Primer::Component.descendants - [Primer::BaseComponent] components_needing_docs = all_components - components components_without_examples = [] args_for_components = [] classes_found_in_examples = [] components.each do |component| documentation = registry.get(component.name) # Primer::AvatarComponent => Avatar short_name = component.name.gsub(/Primer|::|Component/, "") path = Pathname.new("docs/content/components/#{short_name.downcase}.md") path.dirname.mkdir unless path.dirname.exist? File.open(path, "w") do |f| f.puts("---") f.puts("title: #{short_name}") f.puts("status: #{component.status.to_s.capitalize}") f.puts("source: https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb") f.puts("storybook: https://primer.style/view-components/stories/?path=/story/primer-#{short_name.underscore.dasherize}-component") f.puts("---") f.puts f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'") if js_components.include?(component) f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'") f.puts f.puts("") end f.puts f.puts("") f.puts f.puts(view_context.render(inline: documentation.base_docstring)) if documentation.tags(:accessibility).any? f.puts f.puts("## Accessibility") documentation.tags(:accessibility).each do |tag| f.puts f.puts view_context.render(inline: tag.text) end end if documentation.tags(:deprecated).any? f.puts f.puts("## Deprecation") documentation.tags(:deprecated).each do |tag| f.puts f.puts view_context.render(inline: tag.text) end end initialize_method = documentation.meths.find(&:constructor?) if initialize_method.tags(:example).any? f.puts f.puts("## Examples") else components_without_examples << component end initialize_method.tags(:example).each do |tag| (name, description) = tag.name.split("|") f.puts f.puts("### #{name}") if description f.puts f.puts(description) end f.puts html = view_context.render(inline: tag.text) html.scan(/class="([^"]*)"/) do |classnames| classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"}) end f.puts("") f.puts f.puts("```erb") f.puts(tag.text.to_s) f.puts("```") end params = initialize_method.tags(:param) if params.any? f.puts f.puts("## Arguments") f.puts f.puts("| Name | Type | Default | Description |") f.puts("| :- | :- | :- | :- |") args = [] params.each do |tag| params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) } default = if params && params[1] constant_name = "#{component.name}::#{params[1]}" constant_value = constant_name.safe_constantize if constant_value.nil? pretty_value(params[1]) else pretty_value(constant_value) end else "N/A" end args << { "name" => tag.name, "type" => tag.types.join(", "), "default" => default, "description" => view_context.render(inline: tag.text) } f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |") end component_args = { "component" => short_name, "source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb", "parameters" => args } args_for_components << component_args end # Slots V2 docs slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] } if slot_v2_methods.any? f.puts f.puts("## Slots") slot_v2_methods.each do |slot_documentation| f.puts f.puts("### `#{slot_documentation.name.to_s.capitalize}`") if slot_documentation.base_docstring.present? f.puts f.puts(view_context.render(inline: slot_documentation.base_docstring)) end param_tags = slot_documentation.tags(:param) if param_tags.any? f.puts f.puts("| Name | Type | Default | Description |") f.puts("| :- | :- | :- | :- |") end param_tags.each do |tag| params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) } default = if params && params[1] "`#{params[1]}`" else "N/A" end f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |") end end end end end File.open("static/classes.yml", "w") do |f| f.puts YAML.dump(classes_found_in_examples.sort.uniq) end File.open("static/arguments.yml", "w") do |f| f.puts YAML.dump(args_for_components) end # Build system arguments docs from BaseComponent documentation = registry.get(Primer::BaseComponent.name) File.open("docs/content/system-arguments.md", "w") do |f| f.puts("---") f.puts("title: System arguments") f.puts("---") f.puts f.puts("") f.puts f.puts(documentation.base_docstring) f.puts initialize_method = documentation.meths.find(&:constructor?) f.puts(view_context.render(inline: initialize_method.base_docstring)) end puts "Markdown compiled." if components_without_examples.any? puts puts "The following components have no examples defined: #{components_without_examples.map(&:name).join(', ')}. Consider adding an example?" end if components_needing_docs.any? puts puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(', ')}" end end end