lib/cog/embeds.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Cog
  
  # @api developer
  # Methods for querying and manipulating project files
  module Embeds
    
    # Search through all project files for cog embeds, and remember them so that generators can refer to them later
    def gather_from_project
      @embeds ||= {}
      Cog.supported_project_files.each do |filename|
        gather_from_file filename, :hash => @embeds, :type => 'cog'
      end
    end
    
    # @param original [String] file in which to search for keep statements
    # @param scratch [String] file to which keep bodies will be copied (used to create the {EmbedContext} objects)
    # @return [Hash] mapping from hooks to {EmbedContext} objects
    def gather_keeps(original, scratch)
      keeps = {}
      gather_from_file(original, :type => 'keep').each_pair do |hook, count|
        c = keeps[hook] = EmbedContext.new(hook, scratch, count[original])
        raise Errors::DuplicateKeep.new :hook => hook if c.count > 1
        Helpers::FileScanner.scan(original, statement('keep', hook)) do |s|
          c.keep_body = if s.match[4] == '{'
            s.capture_until end_statement('keep')
            s.captured_text
          else
            ''
          end
        end
      end
      keeps
    end
    
    # Copy keep bodies from the original file to the scratch file
    # @param original [String] file in which to search for keep statements. If the original does not exist, then scratch will serve as the original (we do this so that the keeps will get expanded in any case)
    # @param scratch [String] file to which keep bodies will be copied
    # @return [nil]
    def copy_keeps(original, scratch)
      Cog.activate_language(:filename => original) do
        original = scratch unless File.exists? original
        keeps = gather_keeps original, scratch
        keeps.each_pair do |hook, c|
          result = update c, :type => 'keep' do |c|
            c.keep_body
          end
          raise Errors::UnrecognizedKeepHook.new :hook => hook, :filename => original if result.nil?
        end
      end
    end

    # @param filename [String] file from which to gather statements
    # @option opt [Hash] :hash ({}) object in which to gather the mapping
    # @option opt [String] :type ('cog') one of <tt>'cog'</tt> or <tt>'keep'</tt>
    # @return [Hash] mapping from hooks to <tt>{ 'filename' => count }</tt> hashes
    def gather_from_file(filename, opt={})
      bucket = opt[:hash] || {}
      type = opt[:type] || 'cog'
      lang = Cog.language_for filename
      File.read(filename).scan(statement type, '[-A-Za-z0-9_.]+', :lang => lang) do |m|
        hook = m[0]
        bucket[hook] ||= {}
        bucket[hook][filename] ||= 0
        bucket[hook][filename] += 1
      end
      bucket
    end
    
    # @param hook [String] embed hook for which to find directive occurrences
    # @yieldparam context [EmbedContext] describes the context in which the embed statement was found
    def find(hook)
      x = @embeds[hook]
      unless x.nil?
        x.keys.sort.each do |filename|
          c = EmbedContext.new hook, filename, x[filename]
          Cog.activate_language :ext => c.extension do
            c.count.times do |index|
              c.index = index
              yield c
            end
          end
        end
      end
    end
    
    # @param c [EmbedContext] describes the context in which the embed statement was found
    # @option opt [String] :type ('cog') one of <tt>'cog'</tt> or <tt>'keep'</tt>
    # @yieldparam context [EmbedContext] describes the context in which the embed statement was found
    # @yieldreturn [String] the value to substitute into the embed expansion 
    # @return [Boolean,nil] +true+ if the statement was expanded or updated, +false+ if the statement was found, but not changed, +nil+ if it could not be found.
    def update(c, opt={}, &block)
      type = opt[:type] || 'cog'
      Helpers::FileScanner.scan(c.path, statement(type, c.hook), :occurrence => c.actual_index) do |s|
        c.lineno = s.marked_line_number
        c.args = s.match[2].split if s.match[2]
        c.once = !s.match[3].nil?
        if s.match[4] == '{'
          update_body c, s, opt, &block
        else
          expand_body c, s, opt, &block
        end
      end
    end
    
    private
    
    # Pattern groups are
    # * 1 - hook
    # * 2 - args
    # * 3 - once
    # * 4 - expansion begin (curly <tt>{</tt>)
    # @return [Regexp] pattern to match the beginning of a cog embed statement
    def statement(type, hook, opt={})
      lang = opt[:lang] || Cog.active_language
      lang.comment_pattern("#{type}\\s*:\\s*(#{hook})\\s*(?:\\(\\s*(.+?)\\s*\\))?(\\s*once\\s*)?(?:\\s*([{]))?")
    end
    
    # @return [Regexp] pattern to match the end of a cog embed statement
    def end_statement(type = 'cog')
      Cog.active_language.comment_pattern("#{type}\\s*:\\s*[}]")
    end

    # @return [Regexp] pattern to match an line that looks like a cog statement, but is not the end statement
    def anything_but_end(type = 'cog')
      Cog.active_language.comment_pattern("#{type}\\s*:\\s*(?!\\s*[}]).*$")
    end
    
    # @param c [EmbedContext]
    # @param s [FileScanner]
    # @option opt [String] :type ('cog') one of <tt>'cog'</tt> or <tt>'keep'</tt>
    # @return [Boolean] whether or not the scanner updated its file
    def update_body(c, s, opt={}, &block)
      type = opt[:type] || 'cog'
      unless s.capture_until end_statement(type), :but_not => anything_but_end(type)
        raise Errors::SnippetExpansionUnterminated.new :filename => c.path.relative_to_project_root, :line => s.marked_line_number
      end
      c.body = s.captured_text
      value = block.call(c).rstrip
      if c.once? || value.normalize_eol != s.captured_text.rstrip.normalize_eol
        s.replace_captured_text(value + "\n", :once => c.once?)
      else
        false
      end
    end
    
    # @param c [EmbedContext]
    # @param s [FileScanner]
    # @option opt [String] :type ('cog') one of <tt>'cog'</tt> or <tt>'keep'</tt>
    # @return [Boolean] whether or not the scanner updated its file
    def expand_body(c, s, opt={}, &block)
      type = opt[:type] || 'cog'
      lang = Cog.active_language
      value = block.call(c).rstrip
      snip_line = lang.comment "#{c.to_statement type} {"
      unless c.once?
        value = [snip_line, value, lang.comment("#{type}: }")].join("\n")
      end
      s.insert_at_mark(value + "\n")
    end
    
    public
    
    extend self

  end
end