Module: Jazzy::DocBuilder

Defined in:
lib/jazzy/doc_builder.rb,
lib/jazzy/docset_builder.rb

Overview

This module handles HTML generation, file writing, asset copying, and generally building docs given sourcekitten output

Defined Under Namespace

Classes: DocsetBuilder

Class Method Summary collapse

Class Method Details

.build(options) ⇒ SourceModule

Build documentation from the given options

Parameters:

Returns:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/jazzy/doc_builder.rb', line 55

def self.build(options)
  if options.sourcekitten_sourcefile
    stdout = options.sourcekitten_sourcefile.read
  else
    if options.podspec_configured
      stdout = PodspecDocumenter.new(options.podspec).sourcekitten_output
    else
      stdout = Dir.chdir(options.source_directory) do
        arguments = SourceKitten.arguments_from_options(options)
        SourceKitten.run_sourcekitten(arguments)
      end
    end
    unless $?.success?
      warn 'Please pass in xcodebuild arguments using -x'
      warn 'If build arguments are correct, please file an issue on ' \
        'https://github.com/realm/jazzy/issues'
      exit $?.exitstatus || 1
    end
  end

  build_docs_for_sourcekitten_output(stdout, options)
end

.build_docs(output_dir, docs, source_module) ⇒ Object

Build & write HTML docs to disk from structured docs array

Parameters:

  • output_dir (String)

    Root directory to write docs

  • docs (Array)

    Array of structured docs

  • options (Config)

    Build options

  • doc_structure (Array)

    @see #doc_structure_for_docs



83
84
85
86
87
88
89
90
91
92
# File 'lib/jazzy/doc_builder.rb', line 83

def self.build_docs(output_dir, docs, source_module)
  each_doc(output_dir, docs) do |doc, path|
    prepare_output_dir(path.parent, false)
    depth = path.relative_path_from(output_dir).each_filename.count - 1
    path_to_root = '../' * depth
    path.open('w') do |file|
      file.write(document(source_module, doc, path_to_root))
    end
  end
end

.build_docs_for_sourcekitten_output(sourcekitten_output, options) ⇒ SourceModule

Build docs given sourcekitten output

Parameters:

  • sourcekitten_output (String)

    Output of sourcekitten command

  • options (Config)

    Build options

Returns:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/jazzy/doc_builder.rb', line 140

def self.build_docs_for_sourcekitten_output(sourcekitten_output, options)
  (docs, coverage, undocumented) = SourceKitten.parse(
    sourcekitten_output,
    options.min_acl,
    options.skip_undocumented,
    DocumentationGenerator.source_docs)

  prepare_output_dir(options.output, options.clean)
  write_lint_report(undocumented, options)

  unless options.skip_documentation
    build_site(docs, coverage, options)
  end
end

.build_site(docs, coverage, options) ⇒ Object



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
# File 'lib/jazzy/doc_builder.rb', line 109

def self.build_site(docs, coverage, options)
  warn 'building site'

  structure = doc_structure_for_docs(docs)

  docs << SourceDocument.new.tap do |sd|
    sd.name = 'index'
    sd.children = []
    sd.type = SourceDeclaration::Type.new 'document.markdown'
    sd.readme_path = options.readme_path
  end

  source_module = SourceModule.new(options, docs, structure, coverage)

  output_dir = options.output
  build_docs(output_dir, source_module.docs, source_module)

  copy_assets(output_dir)

  DocsetBuilder.new(output_dir, source_module).build!

  friendly_path = relative_path_if_inside(output_dir, Pathname.pwd)
  puts "jam out ♪♫ to your fresh new docs in `#{friendly_path}`"

  source_module
end

.copy_assets(destination) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/jazzy/doc_builder.rb', line 233

def self.copy_assets(destination)
  assets_directory = Config.instance.theme_directory + 'assets'
  FileUtils.cp_r(assets_directory.children, destination)
  Pathname.glob(destination + 'css/**/*.scss').each do |scss|
    contents = scss.read
    css = Sass::Engine.new(contents, syntax: :scss).render
    css_filename = scss.sub(/\.scss$/, '')
    css_filename.open('w') { |f| f.write(css) }
    FileUtils.rm scss
  end
end

.decl_for_token(token) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/jazzy/doc_builder.rb', line 164

def self.decl_for_token(token)
  if token['key.parsed_declaration']
    token['key.parsed_declaration']
  elsif token['key.annotated_decl']
    token['key.annotated_decl'].gsub(/<[^>]+>/, '')
  elsif token['key.name']
    token['key.name']
  else
    'unknown declaration'
  end
end

.doc_structure_for_docs(docs) ⇒ Array

Generate doc structure to be used in sidebar navigation

Returns:

  • (Array)

    doc structure comprised of section names & child names & URLs



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/jazzy/doc_builder.rb', line 32

def self.doc_structure_for_docs(docs)
  docs.map do |doc|
    children = doc.children
                  .sort_by { |c| [c.nav_order, c.name] }
                  .flat_map do |child|
      # FIXME: include arbitrarily nested extensible types
      [{ name: child.name, url: child.url }] +
        Array(child.children.select do |sub_child|
          sub_child.type.swift_extensible?
        end).map do |sub_child|
          { name: "#{sub_child.name}", url: sub_child.url }
        end
    end
    {
      section: doc.name,
      children: children,
    }
  end
end

.document(source_module, doc_model, path_to_root) ⇒ Object

Build Mustache document from single parsed doc

Parameters:

  • options (Config)

    Build options

  • doc_model (Hash)

    Parsed doc. @see SourceKitten.parse

  • path_to_root (String)
  • doc_structure (Array)

    doc structure comprised of section names and child names and URLs. @see doc_structure_for_docs



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/jazzy/doc_builder.rb', line 357

def self.document(source_module, doc_model, path_to_root)
  if doc_model.type.kind == 'document.markdown'
    return document_markdown(source_module, doc_model, path_to_root)
  end

  doc = Doc.new # Mustache model instance
  doc[:custom_head] = Config.instance.custom_head
  doc[:doc_coverage] = source_module.doc_coverage unless
    Config.instance.hide_documentation_coverage
  doc[:name] = doc_model.name
  doc[:kind] = doc_model.type.name
  doc[:dash_type] = doc_model.type.dash_type
  doc[:declaration] = doc_model.declaration
  doc[:overview] = Jazzy.markdown.render(doc_model.overview)
  doc[:structure] = source_module.doc_structure
  doc[:tasks] = render_tasks(source_module, doc_model.children)
  doc[:module_name] = source_module.name
  doc[:author_name] = source_module.author_name
  doc[:github_url] = source_module.github_url
  doc[:dash_url] = source_module.dash_url
  doc[:path_to_root] = path_to_root
  doc.render.gsub(ELIDED_AUTOLINK_TOKEN, path_to_root)
end

.document_markdown(source_module, doc_model, path_to_root) ⇒ Object

Build Mustache document from a markdown source file

Parameters:

  • options (Config)

    Build options

  • doc_model (Hash)

    Parsed doc. @see SourceKitten.parse

  • path_to_root (String)
  • doc_structure (Array)

    doc structure comprised of section names and child names and URLs. @see doc_structure_for_docs



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/jazzy/doc_builder.rb', line 251

def self.document_markdown(source_module, doc_model, path_to_root)
  doc = Doc.new # Mustache model instance
  name = doc_model.name == 'index' ? source_module.name : doc_model.name
  doc[:name] = name
  doc[:overview] = Jazzy.markdown.render(doc_model.content(source_module))
  doc[:custom_head] = Config.instance.custom_head
  doc[:doc_coverage] = source_module.doc_coverage unless
    Config.instance.hide_documentation_coverage
  doc[:structure] = source_module.doc_structure
  doc[:module_name] = source_module.name
  doc[:author_name] = source_module.author_name
  doc[:github_url] = source_module.github_url
  doc[:dash_url] = source_module.dash_url
  doc[:path_to_root] = path_to_root
  doc[:hide_name] = true
  doc.render
end

.each_doc(output_dir, docs, &block) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/jazzy/doc_builder.rb', line 94

def self.each_doc(output_dir, docs, &block)
  docs.each do |doc|
    next unless doc.render?
    # Assuming URL is relative to documentation root:
    path = output_dir + (doc.url || "#{doc.name}.html")
    block.call(doc, path)
    next if doc.name == 'index'
    each_doc(
      output_dir,
      doc.children,
      &block
    )
  end
end

.filepath_for_token(token) ⇒ Object



176
177
178
179
180
181
182
# File 'lib/jazzy/doc_builder.rb', line 176

def self.filepath_for_token(token)
  if ENV['JAZZY_INTEGRATION_SPECS']
    Pathname.new(token['key.filepath']).basename.to_s
  else
    token['key.filepath']
  end
end

.gh_token_url(item, source_module) ⇒ Object

Construct Github token URL

Parameters:

  • item (Hash)

    Parsed doc child item

  • options (Config)

    Build options



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/jazzy/doc_builder.rb', line 279

def self.gh_token_url(item, source_module)
  return unless github_prefix = source_module.github_file_prefix
  return unless should_link_to_github(item.file)
  gh_line = if item.start_line && (item.start_line != item.end_line)
              "#L#{item.start_line}-L#{item.end_line}"
            else
              "#L#{item.line}"
            end
  relative_file_path = item.file.realpath.relative_path_from(
    source_module.root_path)
  "#{github_prefix}/#{relative_file_path}#{gh_line}"
end

.line_number_for_token(token) ⇒ Object



184
185
186
187
188
189
190
# File 'lib/jazzy/doc_builder.rb', line 184

def self.line_number_for_token(token)
  if token['key.doc.line']
    token['key.doc.line'] # Objective-C
  else
    token['key.parsed_scope.start'] # Swift
  end
end

.make_task(mark, uid, items) ⇒ Object



321
322
323
324
325
326
327
328
329
# File 'lib/jazzy/doc_builder.rb', line 321

def self.make_task(mark, uid, items)
  {
    name: mark.name,
    uid: URI.encode(uid),
    items: items,
    pre_separator: mark.has_start_dash,
    post_separator: mark.has_end_dash,
  }
end

.prepare_output_dir(output_dir, clean) ⇒ Object

mkdir -p output directory and clean if option is set



24
25
26
27
# File 'lib/jazzy/doc_builder.rb', line 24

def self.prepare_output_dir(output_dir, clean)
  FileUtils.rm_r output_dir if clean && output_dir.directory?
  FileUtils.mkdir_p output_dir
end

.relative_path_if_inside(path, base_path) ⇒ Object



155
156
157
158
159
160
161
162
# File 'lib/jazzy/doc_builder.rb', line 155

def self.relative_path_if_inside(path, base_path)
  relative = path.relative_path_from(base_path)
  if relative.to_path =~ %r{/^..(\/|$)/}
    path
  else
    relative
  end
end

.render_item(item, source_module) ⇒ Object

Build mustache item for a top-level doc

Parameters:

  • item (Hash)

    Parsed doc child item

  • options (Config)

    Build options



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/jazzy/doc_builder.rb', line 295

def self.render_item(item, source_module)
  # Combine abstract and discussion into abstract
  abstract = (item.abstract || '') + (item.discussion || '')
  item_render = {
    name:                       item.name,
    abstract:                   render_markdown(abstract),
    declaration:                item.declaration,
    other_language_declaration: item.other_language_declaration,
    usr:                        item.usr,
    dash_type:                  item.type.dash_type,
    github_token_url:           gh_token_url(item, source_module),
    default_impl_abstract:      render_markdown(item.default_impl_abstract),
    from_protocol_extension:    item.from_protocol_extension,
    return:                     render_markdown(item.return),
    parameters:                 (item.parameters if item.parameters.any?),
    url:                        (item.url if item.children.any?),
    start_line:                 item.start_line,
    end_line:                   item.end_line,
  }
  item_render.reject { |_, v| v.nil? }
end

.render_markdown(markdown) ⇒ Object



317
318
319
# File 'lib/jazzy/doc_builder.rb', line 317

def self.render_markdown(markdown)
  Jazzy.markdown.render(markdown) if markdown
end

.render_tasks(source_module, children) ⇒ Object

Render tasks for Mustache document

Parameters:

  • options (Config)

    Build options

  • doc_model (Hash)

    Parsed doc. @see SourceKitten.parse



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/jazzy/doc_builder.rb', line 334

def self.render_tasks(source_module, children)
  marks = children.map(&:mark).uniq.compact
  mark_names_counts = {}
  marks.map do |mark|
    mark_children = children.select { |child| child.mark == mark }
    items = mark_children.map { |child| render_item(child, source_module) }
    uid = (mark.name || 'Unnamed').to_s
    if mark_names_counts.key?(uid)
      mark_names_counts[uid] += 1
      uid += (mark_names_counts[uid]).to_s
    else
      mark_names_counts[uid] = 1
    end
    make_task(mark, uid, items)
  end
end


269
270
271
272
273
274
# File 'lib/jazzy/doc_builder.rb', line 269

def self.should_link_to_github(file)
  return unless file
  file = file.realpath.to_path
  source_directory = Config.instance.source_directory.to_path
  file.start_with?(source_directory)
end

.warnings_for_tokens(tokens_by_file) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/jazzy/doc_builder.rb', line 192

def self.warnings_for_tokens(tokens_by_file)
  warnings = []
  tokens_by_file.each_key do |file|
    tokens_by_file[file].each do |token|
      warnings << {
        file: filepath_for_token(token),
        line: line_number_for_token(token),
        symbol: token['key.name'],
        symbol_kind: token['key.kind'],
        warning: 'undocumented',
      }
    end
  end
  warnings
end

.write_lint_report(undocumented, options) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/jazzy/doc_builder.rb', line 208

def self.write_lint_report(undocumented, options)
  (options.output + 'undocumented.json').open('w') do |f|
    tokens_by_file = undocumented.group_by do |d|
      if d['key.filepath']
        Pathname.new(d['key.filepath']).basename.to_s
      else
        d['key.modulename'] || ''
      end
    end

    warnings = warnings_for_tokens(tokens_by_file)

    lint_report = {
      warnings: warnings,
      source_directory: (
        if ENV['JAZZY_INTEGRATION_SPECS']
          'Specs'
        else
          options.source_directory
        end),
    }
    f.write(lint_report.to_json)
  end
end