trulia/hologram

View on GitHub
lib/hologram/doc_builder.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require 'hologram/link_helper'

module Hologram
  class DocBuilder
    attr_accessor :source, :destination, :documentation_assets, :dependencies, :index, :base_path, :renderer, :doc_blocks, :pages, :config_yml
    attr_reader :errors
    attr :doc_assets_dir, :output_dir, :input_dir, :header_erb, :footer_erb

    def self.from_yaml(yaml_file, extra_args = [])

      #Change dir so that our paths are relative to the config file
      base_path = Pathname.new(yaml_file)
      yaml_file = base_path.realpath.to_s
      Dir.chdir(base_path.dirname)

      config = YAML::load_file(yaml_file)
      raise SyntaxError if !config.is_a? Hash

      new(config.merge(
        'config_yml' => config,
        'base_path' => Pathname.new(yaml_file).dirname,
        'renderer' => Utils.get_markdown_renderer(config['custom_markdown'])
      ), extra_args)

    rescue SyntaxError, ArgumentError, Psych::SyntaxError
      raise SyntaxError, "Could not load config file, check the syntax or try 'hologram init' to get started"
    end

    def self.setup_dir
      if File.exists?("hologram_config.yml")
        DisplayMessage.warning("Cowardly refusing to overwrite existing hologram_config.yml")
        return
      end

      FileUtils.cp_r INIT_TEMPLATE_FILES, Dir.pwd
      new_files = [
        "hologram_config.yml",
        "doc_assets/",
        "doc_assets/_header.html",
        "doc_assets/_footer.html",
        "code_example_templates/",
        "code_example_templates/markdown_example_template.html.erb",
        "code_example_templates/markdown_table_template.html.erb",
        "code_example_templates/js_example_template.html.erb",
        "code_example_templates/jsx_example_template.html.erb",
      ]
      DisplayMessage.created(new_files)
    end

    def initialize(options, extra_args = [])
      @pages = {}
      @errors = []
      @dependencies = options.fetch('dependencies', nil) || []
      @index = options['index']
      @base_path = options.fetch('base_path', Dir.pwd)
      @renderer = options.fetch('renderer', MarkdownRenderer)
      @source = Array(options['source'])
      @destination = options['destination']
      @documentation_assets = options['documentation_assets']
      @config_yml = options['config_yml']
      @plugins = Plugins.new(options.fetch('config_yml', {}), extra_args)
      @nav_level = options['nav_level'] || 'page'
      @exit_on_warnings = options['exit_on_warnings']
      @code_example_templates = options['code_example_templates']
      @code_example_renderers = options['code_example_renderers']
      @custom_extensions = Array(options['custom_extensions'])
      @ignore_paths = options.fetch('ignore_paths', [])

      if @exit_on_warnings
        DisplayMessage.exit_on_warnings!
      end
    end

    def build
      set_dirs
      return false if !is_valid?

      set_header_footer
      current_path = Dir.pwd
      Dir.chdir(base_path)
      # Create the output directory if it doesn't exist
      if !output_dir
        FileUtils.mkdir_p(destination)
        set_dirs #need to reset output_dir post-creation for build_docs.
      end
      # the real work happens here.
      build_docs
      Dir.chdir(current_path)
      DisplayMessage.success("Build completed. (-: ")
      true
    end

    def is_valid?
      errors.clear
      set_dirs
      validate_source
      validate_destination
      validate_document_assets

      errors.empty?
    end

    private

    def validate_source
      errors << "No source directory specified in the config file" if source.empty?
      source.each do |dir|
        next if real_path(dir)
        errors << "Can not read source directory (#{dir}), does it exist?"
      end
    end

    def validate_destination
      errors << "No destination directory specified in the config" if !destination
    end

    def validate_document_assets
      errors << "No documentation assets directory specified" if !documentation_assets
    end

    def set_dirs
      @output_dir = real_path(destination)
      @doc_assets_dir = real_path(documentation_assets)
      @input_dir = multiple_paths(source)
    end

    def real_path(dir)
      return if !File.directory?(String(dir))
      Pathname.new(dir).realpath
    end

    def multiple_paths dirs
      Array(dirs).map { |dir| real_path(dir) }.compact
    end

    def build_docs
      doc_parser = DocParser.new(input_dir, index, @plugins, nav_level: @nav_level,
                                                             custom_extensions: @custom_extensions,
                                                             ignore_paths: @ignore_paths)
      @pages, @categories = doc_parser.parse

      if index && !@pages.has_key?(index + '.html')
        DisplayMessage.warning("Could not generate index.html, there was no content generated for the category #{index}.")
      end

      warn_missing_doc_assets
      write_docs
      copy_dependencies
      copy_assets
    end

    def copy_assets
      return unless doc_assets_dir
      Dir.foreach(doc_assets_dir) do |item|
        # ignore . and .. directories and files that start with
        # underscore
        next if item == '.' or item == '..' or item.start_with?('_')
        FileUtils.rm "#{output_dir}/#{item}", force: true if File.file?("#{output_dir}/#{item}")
        FileUtils.rm_rf "#{output_dir}/#{item}" if File.directory?("#{output_dir}/#{item}")
        FileUtils.cp_r "#{doc_assets_dir}/#{item}", "#{output_dir}/#{item}"
      end
    end

    def copy_dependencies
      dependencies.each do |dir|
        begin
          dirpath  = Pathname.new(dir).realpath
          if File.directory?("#{dir}")
            FileUtils.rm_r "#{output_dir}/#{dirpath.basename}", force: true
            FileUtils.cp_r "#{dirpath}", "#{output_dir}/#{dirpath.basename}"
          end
        rescue
          DisplayMessage.warning("Could not copy dependency: #{dir}")
        end
      end
    end

    def write_docs
      load_code_example_templates_and_renderers

      renderer_instance = renderer.new(link_helper: link_helper)
      markdown = Redcarpet::Markdown.new(renderer_instance, { fenced_code_blocks: true, tables: true })
      tpl_vars = TemplateVariables.new({categories: @categories, config: @config_yml, pages: @pages})
      #generate html from markdown
      @pages.each do |file_name, page|
        if file_name.nil?
          raise NoCategoryError
        else
          if page[:blocks] && page[:blocks].empty?
            title = ''
          else
            title, _ = @categories.rassoc(file_name)
          end

          tpl_vars.set_args({title: title, file_name: file_name, blocks: page[:blocks]})
          if page.has_key?(:erb)
            write_erb(file_name, page[:erb], tpl_vars.get_binding)
          else
            write_page(file_name, markdown.render(page[:md]), tpl_vars.get_binding)
          end
        end
      end
    end

    def load_code_example_templates_and_renderers
      if @code_example_templates
        CodeExampleRenderer::Template.path_to_custom_example_templates = real_path(@code_example_templates)
      end

      if @code_example_renderers
        CodeExampleRenderer.path_to_custom_example_renderers = real_path(@code_example_renderers)
      end

      CodeExampleRenderer.load_renderers_and_templates
    end

    def link_helper
      @_link_helper ||= LinkHelper.new(@pages.map { |page|
        if not page[1][:blocks].nil?
        {
          name: page[0],
          component_names: page[1][:blocks].map { |component| component[:name] }
        }
        else
        {
          name: page[0],
          component_names: {}
        }
        end
      })
    end

    def write_erb(file_name, content, binding)
      fh = get_fh(output_dir, file_name)
      erb = ERB.new(content)
      fh.write(erb.result(binding))
    ensure
      fh.close
    end

    def write_page(file_name, body, binding)
      fh = get_fh(output_dir, file_name)
      fh.write(header_erb.result(binding)) if header_erb
      fh.write(body)
      fh.write(footer_erb.result(binding)) if footer_erb
    ensure
      fh.close
    end

    def set_header_footer
      # load the markdown renderer we are going to use

      if File.exists?("#{doc_assets_dir}/_header.html")
        @header_erb = ERB.new(File.read("#{doc_assets_dir}/_header.html"))
      elsif File.exists?("#{doc_assets_dir}/header.html")
        @header_erb = ERB.new(File.read("#{doc_assets_dir}/header.html"))
      else
        @header_erb = nil
        DisplayMessage.warning("No _header.html found in documentation assets. Without this your css/header will not be included on the generated pages.")
      end

      if File.exists?("#{doc_assets_dir}/_footer.html")
        @footer_erb = ERB.new(File.read("#{doc_assets_dir}/_footer.html"))
      elsif File.exists?("#{doc_assets_dir}/footer.html")
        @footer_erb = ERB.new(File.read("#{doc_assets_dir}/footer.html"))
      else
        @footer_erb = nil
        DisplayMessage.warning("No _footer.html found in documentation assets. This might be okay to ignore...")
      end
    end

    def get_file_name(str)
      str.gsub(' ', '_').downcase + '.html'
    end

    def get_fh(output_dir, output_file)
      File.open("#{output_dir}/#{output_file}", 'w')
    end

    def warn_missing_doc_assets
      return if doc_assets_dir
      DisplayMessage.warning("Could not find documentation assets at #{documentation_assets}")
    end
  end
end