# Tangerine -- A general-purpose templating system # Copyright (C) 2004 Christian Neukirchen # # Licensed under the same terms as Ruby itself. class Tangerine < String # Version information VERSION = "0.3.1" @@uid = 0 # The generated code. attr_reader :code # The tag library being used. attr_accessor :taglib # Indent the +string+ to +depth+, only subsequent lines. def self.indent(depth, string) string.gsub(/\n^/, "\n" + ' '*depth) end # Create, parse and compile a new Tangerine template given in +template+. def initialize(template, taglib=nil, indent=0) super template @taglib = taglib @indent = indent @uid = (@@uid += 1) @code = "" generate_header generate_code generate_footer compile end # Return a string to be appended to +string+ in order to return code # that performs output escaping. def escaper "" end # Expand the template for +object+ by calling given methods and inline code. # Output gets sent to +output+ using <<. If called with only # one element, a string holding the result is returned. def expand(output, object=nil) if object.nil? object = output output = "" end begin @template.call output, object rescue => exception raise exception.exception("During template expansion:\n" << " #{exception}\n" << " #{Tangerine.indent 4, exception.backtrace.join("\n")}\n") end end # Emit the code header. def generate_header emit_code "lambda { |#{output_variable}, #{object_variable}| " end # Emit the code footer. def generate_footer emit_code "#{output_variable} }" end # Parse the template and generate appropriate output code. def generate_code @cursor = 0 @mark = 0 while @cursor = index(',,', @cursor) # Skip second comma. skip # Do comma escaping? if peek == ?, skip while peek && peek == ?, emit_text selection @mark = @cursor next else emit_text selection end # Skip comment? if peek == ?# @mark = @cursor = index("\n", @cursor) + 1 emit_code "\n" # Keep lineno uptodate. next end # Initialize local variables. method = "" resolve = false indent = get_indentation escape = get_escape ignore_result = get_ignore_result case peek when ?@ # Use taglib ... skip resolve = true tag = get_tag raise 'Tag not resolvable: foo' unless @taglib expansion = @taglib.resolve(tag) when ?( # ... or instance_eval ... skip method = read_to '('[0], ')'[0] when ?[ # Use instance_eval + refhack skip method = "self" + read_to('['[0], ']'[0]) else # ... or send? method = get_tag end # Read block? block = read_block skip if indent == 0 if @indent != 0 i = "Tangerine.indent(#{@indent}, " else i = "(" end else i = "Tangerine.indent(#{indent}, " end if resolve if block # + 1 for | emit_code "#{result_variable} = #{i}" + self.class.new(expansion, @taglib, indent+1).code + ".call('', #{object_variable})); "; else emit_code self.class.new(expansion, @taglib, indent).code + ".call(#{output_variable}, #{object_variable}); "; end elsif method =~ /^[\w_]+[!?]?$/ emit_code "#{result_variable} = #{object_variable}.#{method}; " else emit_code "#{result_variable} = #{object_variable}.instance_eval(#{method.dump}); " end unless ignore_result if block emit_code "#{template_variable} = " + self.class.new(block, @taglib, indent).code emit_code "; ((#{result_variable} && [*(#{result_variable} == true ? [#{object_variable}] : #{result_variable})].each { |d| #{template_variable}.call(#{output_variable}, d)}).to_s#{escape}); " elsif not resolve emit_code "#{output_variable} << #{i}#{result_variable}.to_s#{escape}); " end end @mark = @cursor end @cursor = -1 if @cursor.nil? emit_text selection_to_end end # Compile generated code into a Proc. def compile begin @template = eval @code, nil, "(tangerine)", 1 rescue SyntaxError raise "Internal error during compilation. Please submit a bugreport.\n" << $! end end private def object_variable "_o#{@uid}" end def template_variable "_m#{@uid}" end def result_variable "_r#{@uid}" end def output_variable "_t#{@uid}" end def selection self[@mark...@cursor-1] end def selection_to_end self[@mark..-1] end def peek self[@cursor+1] end def getc @cursor += 1 self[@cursor] end def skip @cursor += 1 end def backskip @cursor -= 1 end def read_block if peek == ?{ skip read_to('{'[0], '}'[0])[1...-1].gsub(/\A\n/, '') else nil end end def get_indentation # Indent like this line? if peek == ?| skip @cursor - (self.rindex("\n", @cursor) || 0) - 3 else 0 end end def get_escape # Disable escaping? if peek == ?! skip "" else escaper end end def get_ignore_result if peek == ?: skip true else false end end def get_tag tag = "" while peek && peek.chr =~ /[\w0-9_?!.]/ tag << getc end # Allow dots, but skip them if last char. if tag[-1] == ?. backskip tag[0..-2] else tag end end # Generate code +c+. def emit_code(c) @code << c end # Generate output code to emit +t+. def emit_text(t) emit_code "#{output_variable} << #{t.dump}; " emit_code "\n" * t.count("\n") end # Read from the current position to +close+, ignoring nested # +open+/+close+. def read_to(open, close) mark = @cursor open = open.chr close = close.chr while @cursor = index(close, @cursor+1) part = self[mark..@cursor] return part if part.count(open) == part.count(close) end raise RuntimeError, "Hit end of template looking for matching `#{close}'." end # Specialized Tangerine for XML generation. # # By default, all values will be escaped suitable for inclusion in # a XML document (< gets <, > gets # >, etc...). # This can be overriden giving the !-flag in a tag. # # Uses String#xml_escape. class XML < Tangerine def escaper ".xml_escape" end end # Specialized Tangerine for RFC822 message generation. # # By default, all values' newlines will be prefixed with whitespace. # This can be overriden giving the !-flag in a tag. class RFC822 < Tangerine def escaper # What could we do further? %{.gsub("\n", "\n ")} end end # Resolve tags by finding files in the FileLib's search path. # # Typical usage: # filelib = Tangerine::FileLib.new("/my/tags", "/my/other/tags") # template = Tangerine.new File.read("/my/template", filelib) class FileLib # Instantiate a new FileLib using +searchpath+ to lookup files. def initialize(*searchpath) @searchpath = searchpath end def resolve(tag) if dir = @searchpath.find { |dir| File.exist? File.join(dir, tag) } File.read(File.join(dir, tag)).gsub(/\n\Z/, '') else raise "Tag not resolvable: #{tag}" end end end end class String # Return a copy of the string escaped suitable for inclusion in XML. # By default, all values will be escaped suitable for inclusion in # a XML document (< gets <, > gets # >, etc...). def xml_escape self.gsub("&", "&"). gsub('"', """). gsub('>', ">"). gsub('<', "<"). gsub("'", "'") end end