Module: Ariadne::Static::GenerateStructure

Defined in:
lib/ariadne/static/generate_structure.rb

Overview

:nodoc:

Constant Summary collapse

SKIP_METHODS =
[:call, :before_render].freeze

Class Method Summary collapse

Class Method Details

.argsObject



164
165
166
# File 'lib/ariadne/static/generate_structure.rb', line 164

def args
  @args ||= Ariadne::Static::GenerateArguments.call(view_context: view_context)
end

.callObject



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ariadne/static/generate_structure.rb', line 15

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.[:title]
    end

    arg_data = args.find do |component_args|
      component_args["component"] == docs.[: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

.each_codespan_in(node, &block) ⇒ Object



264
265
266
267
268
269
270
271
# File 'lib/ariadne/static/generate_structure.rb', line 264

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

.find_fenced_code_ranges_in(str) ⇒ Object



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/ariadne/static/generate_structure.rb', line 234

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

.find_line_starts_in(str) ⇒ Object



253
254
255
256
257
258
259
260
261
262
# File 'lib/ariadne/static/generate_structure.rb', line 253

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

.generate_args_table(args) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ariadne/static/generate_structure.rb', line 175

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

.previewsObject



160
161
162
# File 'lib/ariadne/static/generate_structure.rb', line 160

def previews
  @previews ||= JSON.parse(Static.read(:previews))
end

.registryObject



273
274
275
# File 'lib/ariadne/static/generate_structure.rb', line 273

def registry
  @registry ||= Ariadne::Yard::Registry.make
end

.render_erb_ignoring_markdown_code_fences(markdown_str) ⇒ Object

Renders ERB code to a string, ignoring markdown code fences. For example, consider the following ERB code inside a markdown document:

### Heading “‘erb <%= render(SomeComponent.new) %> “`

<%= some_func(a, b) %>

The ERB renderer does not understand that the fenced code, i.e. the part inside the triple backticks, should not be rendered. It sees the ERB tags both inside and outside the fence and renders them both.

This method renders ERB tags in a markdown string, ignoring any fenced code blocks, so as to prevent rendering fenced ERB code.



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/ariadne/static/generate_structure.rb', line 210

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

.serialize_params(param_tags, component) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/ariadne/static/generate_structure.rb', line 147

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

.view_contextObject



168
169
170
171
172
173
# File 'lib/ariadne/static/generate_structure.rb', line 168

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