lib/yard/templates/template.rb



require 'erb'

module YARD
  module Templates
    module Template
      attr_accessor :class, :section
      attr_reader :options
      
      class << self
        # @return [Array<Module>] a list of modules to be automatically included
        #   into any new template module
        attr_accessor :extra_includes

        # @private
        def included(klass)
          klass.extend(ClassMethods)
        end
      end
      
      self.extra_includes = []
      
      include ErbCache
      include Helpers::BaseHelper
      include Helpers::MethodHelper

      module ClassMethods
        attr_accessor :path, :full_path
        
        def full_paths
          included_modules.inject([full_path]) do |paths, mod|
            paths |= mod.full_paths if mod.respond_to?(:full_paths)
            paths
          end
        end
    
        def initialize(path, full_paths)
          full_path = full_paths.shift
          self.path = path
          self.full_path = full_path
          include_inherited(full_paths)
          include_parent
          load_setup_rb
        end
      
        # Searches for a file identified by +basename+ in the template's
        # path as well as any mixed in template paths. Equivalent to calling
        # {ClassMethods#find_nth_file} with index of 1.
        # 
        # @param [String] basename the filename to search for
        # @return [String] the full path of a file on disk with filename
        #   +basename+ in one of the template's paths.
        # @see find_nth_file
        def find_file(basename)
          find_nth_file(basename)
        end
        
        # Searches for the nth file (where n = +index+) identified
        # by basename in the template's path and any mixed in template paths.
        # 
        # @param [String] basename the filename to search for
        # @param [Fixnum] index the nth existing file to return
        # @return [String] the full path of the nth file on disk with
        #   filename +basename+ in one of the template paths
        def find_nth_file(basename, index = 1)
          n = 1
          full_paths.each do |path|
            file = File.join(path, basename)
            if File.file?(file)
              return file if index == n
              n += 1
            end
          end
          
          nil
        end

        def is_a?(klass)
          return true if klass == Template
          super(klass)
        end

        # Creates a new template object to be rendered with {Template#run}
        def new(*args)
          obj = Object.new.extend(self)
          obj.class = self
          obj.send(:initialize, *args)
          obj
        end
      
        def run(*args)
          new(*args).run
        end
      
        def T(*path)
          Engine.template(*path)
        end
        
        # Alias for creating a {Section} with arguments
        # @see Section#initialize
        # @since 0.6.0
        def S(*args)
          Section.new(*args)
        end
      
        private

        def include_parent
          pc = path.to_s.split('/')
          if pc.size > 1
            pc.pop
            pc = pc.join('/')
            begin
              include Engine.template(pc)
            rescue ArgumentError
              include Engine.template!(pc, full_path.gsub(%r{/[^/]+$}, ''))
            end
          end
        end
        
        def include_inherited(full_paths)
          full_paths.reverse.each do |full_path|
            include Engine.template!(path, full_path)
          end
        end

        def load_setup_rb
          setup_file = File.join(full_path, 'setup.rb')
          if File.file? setup_file
            module_eval(File.read(setup_file).taint, setup_file, 1)
          end
        end
      end
    
      def initialize(opts = {})
        @cache, @cache_filename = {}, {}
        @sections, @options = [], {}
        add_options(opts)
        
        extend(Helpers::HtmlHelper) if options[:format] == :html
        extend(Helpers::TextHelper) if options[:format] == :text
        extend(Helpers::UMLHelper) if options[:format] == :dot
        extend(*Template.extra_includes) unless Template.extra_includes.empty?

        init
      end
    
      # Loads a template specified by path. If +:template+ or +:format+ is
      # specified in the {#options} hash, they are prepended and appended
      # to the path respectively.
      # 
      # @param [Array<String, Symbol>] path the path of the template
      # @return [Template] the loaded template module
      def T(*path)
        path.unshift(options[:template]) if options[:template]
        path.push(options[:format]) if options[:format]
        self.class.T(*path)
      end
    
      # Sets the sections (and subsections) to be rendered for the template
      # 
      # @example Sets a set of erb sections
      #   sections :a, :b, :c # searches for a.erb, b.erb, c.erb
      # @example Sets a set of method and erb sections
      #   sections :a, :b, :c # a is a method, the rest are erb files
      # @example Sections with subsections
      #   sections :header, [:name, :children]
      #   # the above will call header.erb and only renders the subsections
      #   # if they are yielded by the template (see #yieldall)
      # @param [Array<Symbol, String, Template, Array>] args the sections
      #   to use to render the template. For symbols and strings, the
      #   section will be executed as a method (if one exists), or rendered 
      #   from the file "name.erb" where name is the section name. For 
      #   templates, they will have {Template::ClassMethods#run} called on them. 
      #   Any subsections can be yielded to using yield or {#yieldall}
      def sections(*args)
        @sections = Section.new(nil, *args) if args.size > 0
        @sections
      end
      
      # Initialization called on the template. Override this in a 'setup.rb'
      # file in the template's path to implement a template
      # 
      # @example A default set of sections
      #   def init
      #     sections :section1, :section2, [:subsection1, :etc]
      #   end
      # @see #sections
      def init
      end
    
      # Runs a template on +sects+ using extra options. This method should
      # not be called directly. Instead, call the class method {ClassMethods#run}
      # 
      # @param [Hash, nil] opts any extra options to apply to sections
      # @param [Section, Array] sects a section list of sections to render
      # @param [Fixnum] start_at the index in the section list to start from
      # @param [Boolean] break_first if true, renders only the first section
      # @yield [opts] calls for the subsections to be rendered
      # @yieldparam [Hash] opts any extra options to yield
      # @return [String] the rendered sections joined together
      def run(opts = nil, sects = sections, start_at = 0, break_first = false, &block)
        out = ""
        return out if sects.nil?
        sects = sects[start_at..-1] if start_at > 0
        sects = Section.new(nil, sects) unless sects.is_a?(Section)
        add_options(opts) do
          sects.each do |s|
            self.section = s
            subsection_index = 0
            value = render_section(section) do |*args|
              value = with_section do
                run(args.first, section, subsection_index, true, &block)
              end
              subsection_index += 1 
              value
            end
            out << (value || "")
            break if break_first
          end
        end
        out
      end
         
      # Yields all subsections with any extra options
      # 
      # @param [Hash] opts extra options to be applied to subsections
      def yieldall(opts = nil, &block)
        with_section { run(opts, section, &block) }
      end
    
      # @param [String, Symbol] section the section name
      # @yield calls subsections to be rendered
      # @return [String] the contents of the ERB rendered section
      def erb(section, &block)
        method_name = ErbCache.method_for(cache_filename(section)) do
          erb_with(cache(section), cache_filename(section))
        end
        send(method_name, &block)
      end
      
      # Returns the contents of a file. If +allow_inherited+ is set to +true+,
      # use +{{{__super__}}}+ inside the file contents to insert the contents
      # of the file from an inherited template. For instance, if +templates/b+
      # inherits from +templates/a+ and file "test.css" exists in both directories,
      # both file contents can be retrieved by having +templates/b/test.css+ look
      # like:
      # 
      #   {{{__super__}}}
      #   ...
      #   body { css styles here }
      #   p.class { other styles }
      # 
      # @param [String] basename the name of the file
      # @param [Boolean] allow_inherited whether inherited templates can
      #   be inserted with +{{{__super__}}}+
      # @return [String] the contents of a file identified by +basename+. All
      #   template paths (including any mixed in templates) are searched for
      #   the file
      # @see ClassMethods#find_file 
      # @see ClassMethods#find_nth_file
      def file(basename, allow_inherited = false)
        file = self.class.find_file(basename)
        raise ArgumentError, "no file for '#{basename}' in #{self.class.path}" unless file

        data = IO.read(file)
        if allow_inherited
          superfile = self.class.find_nth_file(basename, 2)
          data.gsub!('{{{__super__}}}', superfile ? IO.read(superfile) : "")
        end

        data
      end
      
      # Calls the ERB file from the last inherited template with {#section}.erb
      # 
      # @param [Symbol, String] section if provided, uses a specific section name
      # @return [String] the rendered ERB file in any of the inherited template
      #   paths.
      def superb(section = section, &block)
        filename = self.class.find_nth_file(erb_file_for(section), 2)
        return "" unless filename
        method_name = ErbCache.method_for(filename) { erb_with(IO.read(filename), filename) }
        send(method_name, &block)
      end
      
      def options=(value)
        @options = value
        set_ivars
      end
      
      def inspect
        "Template(#{self.class.path}) [section=#{section.name}]"
      end
    
      protected
    
      def erb_file_for(section)
        "#{section}.erb"
      end
      
      def erb_with(content, filename = nil)
        erb = ERB.new(content, nil, options[:format] == :text ? '<>' : nil)
        erb.filename = filename if filename
        erb
      end
    
      private
    
      def render_section(section, &block)
        section = section.name if section.is_a?(Section)
        case section
        when Section, String, Symbol
          if respond_to?(section)
            send(section, &block) 
          else
            erb(section, &block)
          end
        when Module, Template
          section.run(options, &block) if section.is_a?(Template)
        end || ""
      end

      def cache(section)
        content = @cache[section.to_sym]
        return content if content
      
        file = cache_filename(section)
        @cache_filename[section.to_sym] = file
        raise ArgumentError, "no template for section '#{section}' in #{self.class.path}" unless file
        @cache[section.to_sym] = IO.read(file)
      end
      
      def cache_filename(section)
        @cache_filename[section.to_sym] ||=
          self.class.find_file(erb_file_for(section))
      end
      
      def set_ivars
        options.each do |k, v|
          instance_variable_set("@#{k}", v)
        end
      end
    
      def add_options(opts = nil)
        return(yield) if opts.nil? && block_given?
        cur_opts = options if block_given?
        
        self.options = options.merge(opts)
      
        if block_given?
          value = yield
          self.options = cur_opts 
          value
        end
      end
      
      def with_section(&block)
        sect = section
        value = yield
        self.section = sect
        value
      end
    end
  end
end