module Ariadne::Static::GenerateStructure
def args
def args @args ||= Ariadne::Static::GenerateArguments.call(view_context: view_context) end
def call
def call components = Ariadne::BaseComponent.descendants.sort_by(&:name) - [Ariadne::BaseComponent] component_docs = components.each_with_object({}) do |component, memo| docs = registry.find(component) preview_data = previews.find do |preview| preview["component"] == docs.metadata[:title] end arg_data = args.find do |component_args| component_args["component"] == docs.metadata[:title] end slot_docs = docs.slot_methods.map do |slot_method| param_tags = slot_method.tags(:param) description = if slot_method.base_docstring.to_s.present? render_erb_ignoring_markdown_code_fences(slot_method.base_docstring).force_encoding("UTF-8") else "" end { "name" => slot_method.name, "description" => description, "parameters" => serialize_params(param_tags, component), } end mtds = docs.non_slot_methods.select do |mtd| next false if mtd.base_docstring.to_s.blank? next false if SKIP_METHODS.include?(mtd.name) method_location, = mtd.files.first class_location, = docs.docs.files.first method_location == class_location end method_docs = mtds.map do |mtd| param_tags = mtd.tags(:param) { "name" => mtd.name, "description" => render_erb_ignoring_markdown_code_fences(mtd.base_docstring), "parameters" => serialize_params(param_tags, component), } end description = if component == Ariadne::BaseComponent docs.base_docstring else render_erb_ignoring_markdown_code_fences(docs.base_docstring) end accessibility_docs = if (accessibility_tag_text = docs.tags(:accessibility)&.first&.text) render_erb_ignoring_markdown_code_fences(accessibility_tag_text) end behavior_docs = if (behavior_tag_text = docs.tags(:behaviors)&.first&.text) render_erb_ignoring_markdown_code_fences(behavior_tag_text) end memo[component.name] = { "fully_qualified_name" => component.name, "description" => description, "accessibility_docs" => accessibility_docs, "behavior_docs" => behavior_docs, "is_form_component" => docs.manifest_entry.form_component?, "requires_js" => docs.manifest_entry.requires_js?, **arg_data, "slots" => slot_docs, "methods" => method_docs, "previews" => (preview_data || {}).fetch("examples", []), "subcomponents" => [], } end Ariadne::BaseComponent.descendants.sort_by(&:name).each do |component| fq_class = component.name.to_s.split("::") fq_class.shift # remove Ariadne:: type = fq_class.shift # remove {UI,Form,*}:: parent, *child = *fq_class next if child.empty? || child.length < 2 parent_class = "Ariadne::#{type}".constantize parent_class = parent_class.const_get(parent) parent_docs = component_docs["#{parent_class}::Component"] next unless parent_docs if (child_docs = component_docs.delete(component.name)) parent_docs["subcomponents"] << child_docs end end toc_categories = { "UI" => [], "Form" => [], "Layout" => [], "Behaviors" => [], } component_docs.values.each do |component| type, name = component["short_name"].split("::", 2) next unless toc_categories[type] # not a required category # removes children from toc, like `Ariadne::UI::Accordion::Item`, # by not adding the component to the TOC if it has more than one level of nesting next if name.include?("::") slug = component["short_name"].gsub("::", "/").downcase toc_categories[type] << { "name" => name, "slug" => slug, } File.open(File.join(DEFAULT_STATIC_PATH, FILE_NAMES[:toc]), "w") do |f| f.write(JSON.pretty_generate(toc_categories)) f.write($INPUT_RECORD_SEPARATOR) end end component_docs.values end
def each_codespan_in(node, &block)
def each_codespan_in(node, &block) return unless node.respond_to?(:children) node.children.each do |child| yield child if child.type == :codespan each_codespan_in(child, &block) end end
def find_fenced_code_ranges_in(str)
def find_fenced_code_ranges_in(str) doc = Kramdown::Document.new(str) line_starts = find_line_starts_in(str) [].tap do |code_ranges| each_codespan_in(doc.root) do |node| options = node.options delimiter = options[:codespan_delimiter] next unless delimiter.start_with?("```") start_pos = line_starts[options[:location]] end_pos = start_pos + node.value.size + delimiter.size end_pos = str.index("```", end_pos) + 3 code_ranges << (start_pos...end_pos) end end end
def find_line_starts_in(str)
def find_line_starts_in(str) line_counter = 2 { 1 => 0 }.tap do |memo| str.scan(/\r?\n/) do memo[line_counter] = Regexp.last_match.end(0) line_counter += 1 end end end
def generate_args_table(args)
def generate_args_table(args) rows = args.map do |arg| parts = [ "`#{arg.name}`", arg.type, arg.description.squish, ] "| #{parts.join(" | ")} |" end <<~MARKDOWN | Name | Type | Description | | :- | :- | :- | #{rows.join("\n")} MARKDOWN end
def previews
def previews @previews ||= JSON.parse(Static.read(:previews)) end
def registry
def registry @registry ||= Ariadne::Yard::Registry.make end
def render_erb_ignoring_markdown_code_fences(markdown_str)
to prevent rendering fenced ERB code.
This method renders ERB tags in a markdown string, ignoring any fenced code blocks, so as
and renders them both.
backticks, should not be rendered. It sees the ERB tags both inside and outside the fence
The ERB renderer does not understand that the fenced code, i.e. the part inside the triple
<%= some_func(a, b) %>
```
<%= render(SomeComponent.new) %>
```erb
### Heading
following ERB code inside a markdown document:
Renders ERB code to a string, ignoring markdown code fences. For example, consider the
def render_erb_ignoring_markdown_code_fences(markdown_str) return view_context.render(inline: markdown_str) if markdown_str.exclude?("```") # identify all fenced code blocks in markdown string code_ranges = find_fenced_code_ranges_in(markdown_str) # replace code fences with placeholders de_fenced_markdown_str = markdown_str.dup.tap do |memo| code_ranges.reverse_each.with_index do |code_range, idx| memo[code_range] = "<!--codefence#{idx}-->" end end # Render ERB tags. The only ones left will explicitly exist _outside_ markdown code fences. rendered_str = view_context.render(inline: de_fenced_markdown_str) # replace placeholders with original code fences code_ranges.reverse_each.with_index do |code_range, idx| rendered_str.sub!("<!--codefence#{idx}-->", markdown_str[code_range]) end rendered_str end
def serialize_params(param_tags, component)
def serialize_params(param_tags, component) param_tags.map do |tag| default_value = Ariadne::Yard::DocsHelper.pretty_default_value(tag, component) { "name" => tag.name, "type" => tag.types&.join(", ") || "", "default" => default_value, "description" => render_erb_ignoring_markdown_code_fences(tag.text.squish), } end end
def view_context
def view_context @view_context ||= ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context.tap do |vc| vc.singleton_class.include(Ariadne::Yard::StructureDocsHelper) vc.singleton_class.include(Ariadne::ViewHelper) end end