module Jazzy::DocBuilder
This module handles HTML generation, file writing, asset copying, and generally building docs given sourcekitten output
Public Class Methods
Build documentation from the given options @param [Config] options
# File lib/jazzy/doc_builder.rb, line 70 def self.build(options) module_jsons = options.module_configs.map do |module_config| if module_config.podspec_configured # Config#validate guarantees not multi-module here pod_documenter = PodspecDocumenter.new(options.podspec) pod_documenter.sourcekitten_output(options) elsif !module_config.sourcekitten_sourcefile.empty? "[#{module_config.sourcekitten_sourcefile.map(&:read).join(',')}]" elsif module_config.swift_build_tool == :symbolgraph SymbolGraph.build(module_config) else Dir.chdir(module_config.source_directory) do arguments = SourceKitten.arguments_from_options(module_config) SourceKitten.run_sourcekitten(arguments) end end end build_docs_for_sourcekitten_output(module_jsons, options) end
Build & write HTML docs to disk from structured docs array @param [String] output_dir Root directory to write docs @param [SourceModule] source_module All info to generate docs
# File lib/jazzy/doc_builder.rb, line 94 def self.build_docs(output_dir, source_module) each_doc(output_dir, source_module.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 given sourcekitten output @param [Array<String>] sourcekitten_output Output of sourcekitten command for each module @param [Config] options Build options
# File lib/jazzy/doc_builder.rb, line 156 def self.build_docs_for_sourcekitten_output(sourcekitten_output, options) (docs, stats) = SourceKitten.parse( sourcekitten_output, options, DocumentationGenerator.source_docs, ) prepare_output_dir(options.output, options.clean) stats.report unless options.skip_documentation build_site(docs, stats.doc_coverage, options) end write_lint_report(stats.undocumented_decls, options) end
# File lib/jazzy/doc_builder.rb, line 120 def self.build_site(docs, coverage, options) warn 'building site' structure = doc_structure_for_docs(docs) docs << SourceDocument.make_index(options.readme_path) output_dir = options.output docset_builder = DocsetBuilder.new(output_dir) source_module = SourceModule.new(docs, structure, coverage, docset_builder) build_docs(output_dir, source_module) unless options.disable_search warn 'building search index' SearchBuilder.build(source_module, output_dir) end copy_extensions(source_module, output_dir) copy_theme_assets(output_dir) docset_builder.build!(source_module.all_declarations) generate_badge(source_module.doc_coverage, options) friendly_path = relative_path_if_inside(output_dir, Pathname.pwd) puts "jam out ♪♫ to your fresh new docs in `#{friendly_path}`" source_module end
# File lib/jazzy/doc_builder.rb, line 54 def self.children_for_doc(doc) doc.children .sort_by { |c| [c.nav_order, c.name, c.usr || ''] } .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? || sub_child.type.extension? end).map do |sub_child| { name: "– #{sub_child.name}", url: sub_child.url } end end end
Returns the appropriate color for the provided percentage, used for generating a badge on shields.io @param [Number] coverage The documentation coverage percentage
# File lib/jazzy/doc_builder.rb, line 286 def self.color_for_coverage(coverage) if coverage < 10 'e05d44' # red elsif coverage < 30 'fe7d37' # orange elsif coverage < 60 'dfb317' # yellow elsif coverage < 85 'a4a61d' # yellowgreen elsif coverage < 90 '97CA00' # green else '4c1' # brightgreen end end
# File lib/jazzy/doc_builder.rb, line 230 def self.copy_extension(name, destination) ext_directory = Pathname(__dir__) / 'extensions' / name FileUtils.cp_r(ext_directory.children, destination) end
# File lib/jazzy/doc_builder.rb, line 223 def self.copy_extensions(source_module, destination) if source_host = source_module.host&.extension copy_extension(source_host, destination) end copy_extension('katex', destination) if Markdown.has_math end
# File lib/jazzy/doc_builder.rb, line 212 def self.copy_theme_assets(destination) assets_directory = Config.instance.theme_directory + 'assets' FileUtils.cp_r(assets_directory.children, destination) Pathname.glob(destination + 'css/**/*.scss').each do |scss| css = SassC::Engine.new(scss.read).render css_filename = scss.sub(/\.scss$/, '') css_filename.open('w') { |f| f.write(css) } FileUtils.rm scss end end
Generate doc structure to be used in sidebar navigation @return [Array] doc structure comprised of
section names & child names & URLs
# File lib/jazzy/doc_builder.rb, line 34 def self.doc_structure_for_docs(docs) docs .map do |doc| children = children_for_doc(doc) { section: doc.name, url: doc.url, children: children, } end .select do |structure| if Config.instance.hide_unlisted_documentation unlisted_prefix = Config.instance.custom_categories_unlisted_prefix structure[:section] != "#{unlisted_prefix}Guides" else true end end end
rubocop:disable Metrics/MethodLength Build Mustache document from single parsed decl @param [SourceModule] module-wide settings @param [Hash] doc_model Parsed doc. @see SourceKitten.parse
@param [String] path_to_root
# File lib/jazzy/doc_builder.rb, line 423 def self.document(source_module, doc_model, path_to_root) if doc_model.type.markdown? return document_markdown(source_module, doc_model, path_to_root) end overview = (doc_model.abstract || '') + (doc_model.discussion || '') alternative_abstract = doc_model.alternative_abstract if alternative_abstract overview = render(doc_model, alternative_abstract) + overview end doc = new_document(source_module, doc_model) doc[:name] = doc_model.name doc[:kind] = doc_model.type.name doc[:dash_type] = doc_model.type.dash_type doc[:declaration] = doc_model.display_declaration doc[:language] = doc_model.display_language doc[:other_language_declaration] = doc_model.display_other_language_declaration doc[:overview] = overview doc[:parameters] = doc_model.parameters doc[:return] = doc_model.return doc[:tasks] = render_tasks(source_module, doc_model.children) doc[:path_to_root] = path_to_root doc[:deprecation_message] = doc_model.deprecation_message doc[:unavailable_message] = doc_model.unavailable_message doc[:usage_discouraged] = doc_model.usage_discouraged? doc.render.gsub(ELIDED_AUTOLINK_TOKEN, path_to_root) end
Build Mustache document from a markdown source file @param [SourceModule] module-wide settings @param [Hash] doc_model Parsed doc. @see SourceKitten.parse
@param [String] path_to_root
# File lib/jazzy/doc_builder.rb, line 273 def self.document_markdown(source_module, doc_model, path_to_root) doc = new_document(source_module, doc_model) name = doc_model.readme? ? source_module.readme_title : doc_model.name doc[:name] = name doc[:overview] = render(doc_model, doc_model.content(source_module)) doc[:path_to_root] = path_to_root doc[:hide_name] = true doc.render.gsub(ELIDED_AUTOLINK_TOKEN, path_to_root) end
# File lib/jazzy/doc_builder.rb, line 105 def self.each_doc(output_dir, docs, &block) docs.each do |doc| next unless doc.render_as_page? # Filepath is relative to documentation root: path = output_dir + doc.filepath block.call(doc, path) each_doc( output_dir, doc.children, &block ) end end
Generates an SVG similar to those from shields.io displaying the documentation percentage @param [Number] coverage The documentation coverage percentage @param [Config] options Build options
# File lib/jazzy/doc_builder.rb, line 308 def self.generate_badge(coverage, options) return if options.hide_documentation_coverage coverage_length = coverage.to_s.size.succ percent_string_length = coverage_length * 80 + 10 percent_string_offset = coverage_length * 40 + 975 width = coverage_length * 8 + 104 svg = <<-SVG.gsub(/^ {8}/, '') <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="#{width}" height="20"> <linearGradient id="b" x2="0" y2="100%"> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="1" stop-opacity=".1"/> </linearGradient> <clipPath id="a"> <rect width="#{width}" height="20" rx="3" fill="#fff"/> </clipPath> <g clip-path="url(#a)"> <path fill="#555" d="M0 0h93v20H0z"/> <path fill="##{color_for_coverage(coverage)}" d="M93 0h#{percent_string_length / 10 + 10}v20H93z"/> <path fill="url(#b)" d="M0 0h#{width}v20H0z"/> </g> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="830"> documentation </text> <text x="475" y="140" transform="scale(.1)" textLength="830"> documentation </text> <text x="#{percent_string_offset}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="#{percent_string_length}"> #{coverage}% </text> <text x="#{percent_string_offset}" y="140" transform="scale(.1)" textLength="#{percent_string_length}"> #{coverage}% </text> </g> </svg> SVG badge_output = options.output + 'badge.svg' File.open(badge_output, 'w') { |f| f << svg } end
rubocop:enable Metrics/MethodLength
# File lib/jazzy/doc_builder.rb, line 387 def self.make_task(mark, uid, items, doc_model) { name: mark.name, name_html: (render_inline(doc_model, mark.name) if mark.name), uid: ERB::Util.url_encode(uid), items: items, pre_separator: mark.has_start_dash, post_separator: mark.has_end_dash, } end
Build Mustache document - common fields between page types
# File lib/jazzy/doc_builder.rb, line 246 def self.new_document(source_module, doc_model) Doc.new.tap do |doc| doc[:custom_head] = Config.instance.custom_head doc[:disable_search] = Config.instance.disable_search doc[:doc_coverage] = source_module.doc_coverage unless Config.instance.hide_documentation_coverage doc[:structure] = source_module.doc_structure doc[:readme_title] = source_module.readme_title doc[:module_name] = doc[:readme_title] doc[:author_name] = source_module.author_name if source_host = source_module.host doc[:source_host_name] = source_host.name doc[:source_host_url] = source_host.url doc[:source_host_image] = source_host.image doc[:source_host_item_url] = source_host.item_url(doc_model) doc[:github_url] = doc[:source_host_url] doc[:github_token_url] = doc[:source_host_item_url] end doc[:dash_url] = source_module.dash_feed_url doc[:breadcrumbs] = make_breadcrumbs(doc_model) end end
mkdir -p output directory and clean if option is set
# File lib/jazzy/doc_builder.rb, line 26 def self.prepare_output_dir(output_dir, clean) FileUtils.rm_r output_dir if clean && output_dir.directory? FileUtils.mkdir_p output_dir end
# File lib/jazzy/doc_builder.rb, line 174 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
# File lib/jazzy/doc_builder.rb, line 235 def self.render(doc_model, markdown) html = Markdown.render(markdown) SourceKitten.autolink_document(html, doc_model) end
# File lib/jazzy/doc_builder.rb, line 240 def self.render_inline(doc_model, markdown) html = Markdown.render_inline(markdown) SourceKitten.autolink_document(html, doc_model) end
Build mustache item for a top-level doc @param [Hash] item Parsed doc child item @param [Config] options Build options rubocop:disable Metrics/MethodLength
# File lib/jazzy/doc_builder.rb, line 355 def self.render_item(item, source_module) # Combine abstract and discussion into abstract abstract = (item.abstract || '') + (item.discussion || '') source_host_item_url = source_module.host&.item_url(item) { name: item.name, name_html: item.name.gsub(':', ':<wbr>'), abstract: abstract, declaration: item.display_declaration, language: item.display_language, other_language_declaration: item.display_other_language_declaration, usr: item.usr, dash_type: item.type.dash_type, source_host_item_url: source_host_item_url, github_token_url: source_host_item_url, default_impl_abstract: item.default_impl_abstract, from_protocol_extension: item.from_protocol_extension, return: item.return, parameters: (item.parameters if item.parameters.any?), url: (item.url if item.render_as_page?), start_line: item.start_line, end_line: item.end_line, direct_link: item.omit_content_from_parent?, deprecation_message: item.deprecation_message, unavailable_message: item.unavailable_message, usage_discouraged: item.usage_discouraged?, async: item.async, declaration_note: item.declaration_note, } end
Render tasks for Mustache document @param [Config] options Build options @param [Hash] doc_model Parsed doc. @see SourceKitten.parse
# File lib/jazzy/doc_builder.rb, line 401 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, mark_children.first) end end
# File lib/jazzy/doc_builder.rb, line 183 def self.undocumented_warnings(decls) decls.map do |decl| { file: decl.file, line: decl.start_line || decl.line, symbol: decl.fully_qualified_name, symbol_kind: decl.type.kind, warning: 'undocumented', } end end
# File lib/jazzy/doc_builder.rb, line 195 def self.write_lint_report(undocumented, options) (options.output + 'undocumented.json').open('w') do |f| warnings = undocumented_warnings(undocumented) lint_report = { warnings: warnings.sort_by do |w| [w[:file] || Pathname(''), w[:line] || 0, w[:symbol], w[:symbol_kind]] end, source_directory: options.source_directory, } f.write(JSON.pretty_generate(lint_report)) end end