lib/temple/generators.rb



module Temple
  # == The Core Abstraction
  #
  # The core abstraction is what every template evetually should be compiled
  # to. Currently it consists of four essential and two convenient types:
  # multi, static, dynamic, block, newline and capture.
  #
  # When compiling, there's two different strings we'll have to think about.
  # First we have the generated code. This is what your engine (from Temple's
  # point of view) spits out. If you construct this carefully enough, you can
  # make exceptions report correct line numbers, which is very convenient.
  #
  # Then there's the result. This is what your engine (from the user's point
  # of view) spits out. It's what happens if you evaluate the generated code.
  #
  # === [:multi, *sexp]
  #
  # Multi is what glues everything together. It's simply a sexp which combines
  # several others sexps:
  #
  #   [:multi,
  #     [:static, "Hello "],
  #     [:dynamic, "@world"]]
  #
  # === [:static, string]
  #
  # Static indicates that the given string should be appended to the result.
  #
  # Example:
  #
  #   [:static, "Hello World"]
  #   # is the same as:
  #   _buf << "Hello World"
  #
  #   [:static, "Hello \n World"]
  #   # is the same as
  #   _buf << "Hello\nWorld"
  #
  # === [:dynamic, ruby]
  #
  # Dynamic indicates that the given Ruby code should be evaluated and then
  # appended to the result.
  #
  # The Ruby code must be a complete expression in the sense that you can pass
  # it to eval() and it would not raise SyntaxError.
  #
  # === [:block, ruby]
  #
  # Block indicates that the given Ruby code should be evaluated, and may
  # change the control flow. Any \n causes a newline in the generated code.
  #
  # === [:newline]
  #
  # Newline causes a newline in the generated code, but not in the result.
  #
  # === [:capture, variable_name, sexp]
  #
  # Evaluates the Sexp using the rules above, but instead of appending to the
  # result, it sets the content to the variable given.
  #
  # Example:
  #
  #   [:multi,
  #     [:static, "Some content"],
  #     [:capture, "foo", [:static, "More content"]],
  #     [:dynamic, "foo.downcase"]]
  #   # is the same as:
  #   _buf << "Some content"
  #   foo = "More content"
  #   _buf << foo.downcase
  class Generator
    include Mixins::Options

    default_options[:buffer] = '_buf'

    def call(exp)
      [preamble, compile(exp), postamble].join(' ; ')
    end

    def compile(exp)
      type, *args = exp
      if respond_to?("on_#{type}")
        send("on_#{type}", *args)
      else
        raise "Generator supports only core expressions - found #{exp.inspect}"
      end
    end

    def on_multi(*exp)
      exp.map { |e| compile(e) }.join(' ; ')
    end

    def on_newline
      "\n"
    end

    def on_capture(name, block)
      options[:capture_generator].new(:buffer => name).call(block)
    end

    protected

    def buffer
      options[:buffer]
    end

    def concat(str)
      "#{buffer} << (#{str})"
    end
  end

  module Generators
    # Implements an array buffer.
    #
    #   _buf = []
    #   _buf << "static"
    #   _buf << dynamic
    #   block do
    #     _buf << "more static"
    #   end
    #   _buf.join
    class ArrayBuffer < Generator
      def preamble
        "#{buffer} = []"
      end

      def postamble
        "#{buffer} = #{buffer}.join"
      end

      def on_static(text)
        concat(text.inspect)
      end

      def on_dynamic(code)
        concat(code)
      end

      def on_block(code)
        code
      end
    end

    # Just like ArrayBuffer, but doesn't call #join on the array.
    class Array < ArrayBuffer
      def postamble
        buffer
      end
    end

    # Implements a string buffer.
    #
    #   _buf = ''
    #   _buf << "static"
    #   _buf << dynamic.to_s
    #   block do
    #     _buf << "more static"
    #   end
    #   _buf
    class StringBuffer < Array
      def preamble
        "#{buffer} = ''"
      end

      def on_dynamic(code)
        concat("(#{code}).to_s")
      end
    end

    # Implements a rails output buffer.
    #
    #   @output_buffer = ActionView::OutputBuffer
    #   @output_buffer.safe_concat "static"
    #   @output_buffer.safe_concat dynamic.to_s
    #   block do
    #     @output_buffer << "more static"
    #   end
    #   @output_buffer
    class RailsOutputBuffer < StringBuffer
      set_default_options :buffer_class => 'ActionView::OutputBuffer',
                          :buffer => '@output_buffer',
                          :capture_generator => RailsOutputBuffer

      def preamble
        "#{buffer} = #{options[:buffer_class]}.new"
      end

      def concat(str)
        "#{buffer}.safe_concat((#{str}))"
      end
    end
  end

  Generator.default_options[:capture_generator] = Temple::Generators::StringBuffer
end