# # Kashmir -- A general-purpose templating system # # Copyright (C) 2005 Christian Neukirchen # # This work is licensed under the same terms as Ruby itself. # class Object def true? yield self end def false? nil end def with yield self end end class FalseClass def true? end def false? yield self end end class NilClass def true? end def false? yield self end end # # Kashmir is an advanced templating engine featuring an easy, # pragmatic syntax and efficient generated code. # # Please see the attached README for an introduction into the # templating language. # class Kashmir < String # The generated code for the template. attr_reader :code # The used taglib (needs to respond to []). attr_reader :taglib # The Proc used to sanitize strings. attr_reader :escaper # The character used to introduce Kashmir commands. ESCAPE_CHAR = ?^ # # Create a new Kashmir template from _template_, possibly using # _taglib_ to retrieve subtemplates and _escaper_ to sanitize # output. # def initialize(template, taglib=nil, escaper=nil) super template unless escaper.nil? or escaper.kind_of?(Proc) raise ArgumentError, "escaper must be a Proc, if given" end @escaper = escaper @taglib = taglib @code = "" generate_code compile_code end # # Return an instace of Kashmir with XML escaping enabled. # By default, all values will be escaped suitable for inclusion in # a XML document (< gets <, > gets # >, etc...). # def self.for_XML(template, taglib=nil) new template, taglib, lambda { |s| s.gsub("&", "&"). gsub('"', """). gsub('>', ">"). gsub('<', "<"). gsub("'", "'") } end # # Expand the template using _object_ as the starting object. If a # _block_ is given, it will be passed to Elusion#new and be used # instead of _object_. # # If _output_ is given, the generated output will be send to # _output_ using <<, else a string with the expansion will be # returned. # def expand(object=nil, output='', &block) if block require 'elusion' unless defined? Elusion object = Elusion.new(&block) end @template.call [[nil], [object]], output, @escaper end def output_variable # :nodoc: "_k_out_" end def stack_variable # :nodoc: "_k_stk_" end def args_variable # :nodoc: "_k_arg_" end def escaper_variable # :nodoc: "_k_esc_" end private def emit_code(code) @code << code end def emit_text(text) return if text.empty? emit_code "#{output_variable} << #{text.dump}; " emit_code "\n" * text.count("\n") end def generate_code @cursor = 0 @mark = 0 while @cursor = index(ESCAPE_CHAR, @cursor) depth = 0 while peek == ESCAPE_CHAR depth += 1 getc # Eat. end emit_text self[@mark..@cursor-depth-1] unless @cursor-depth <= 0 case peek when ?# @mark = @cursor = index("\n", @cursor) + 1 next when ?( skip code = read_to(?(, ?)) else code = get_identifier if code =~ /\A(\d+)(.*)/ code = "self[#$1]#$2" end end block = get_block generate_call code, block, depth skip @mark = @cursor end emit_text self[@mark..-1] end def generate_call(code, block, depth) object = if depth == 0 # .last is faster and will be called most. "#{stack_variable}.last.first" else "#{stack_variable}[#{-depth-1}].first" end if code =~ /\((\w+)\s+(.*)\)/ if Instructions.respond_to? $1 emit_code Instructions.send($1, self, $2) return end end if code =~ /\A[\w.!?]+\Z/ call = code else call = "instance_eval{#{code}}" end if block.nil? if @escaper emit_code "#{output_variable} << " << "#{escaper_variable}.call(#{object}.#{call}.to_s); " else emit_code "#{output_variable} << #{object}.#{call}.to_s; " end else call_block = "{|*#{args_variable}| " << "#{stack_variable}.push #{args_variable}; " << "#{self.class.new(block, @taglib, @escaper).code}" << "#{stack_variable}.pop }" if code =~ /\A[\w.?!]+\Z/ emit_code "#{object}.#{code}#{call_block}; " else emit_code "#{object}.instance_eval{#{code[1...-1]} #{call_block}}; " end end end def compile_code @template = eval "lambda { |#{stack_variable}, #{output_variable}, " << "#{escaper_variable}| " << @code << "#{output_variable} }" end def peek self[@cursor+1] end def getc @cursor += 1 self[@cursor] end def skip @cursor += 1 end def get_identifier identifier = "" while peek && peek.chr =~ /[\w?!.]/ identifier << getc end # Allow dots, but skip them if last char. if identifier[-1] == ?. @cursor -= 1 identifier[0..-2] else identifier end end def get_block if peek == ?{ skip read_to(?{, ?})[1...-1].gsub(/\A\n/, '') else nil end end # Read from the current position to +close+, ignoring nested # occurrences of +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 # # Module to keep Instructions for Kashmir. # module Instructions # # Emit the result of evaluating _string_ to the output without # sanitation. # def self.raw(kashmir, string) "#{kashmir.output_variable} << #{kashmir.stack_variable}." << "last.first.instance_eval{#{string}}.to_s; " end # # Evaluate _string_, but do not generate any output. # def self.ignore(kashmir, string) "#{kashmir.stack_variable}.last.first.instance_eval { #{string} }; nil; " end # # Call the _template_ of the taglib. # def self.call(kashmir, template) raise "no taglib defined" unless kashmir.taglib Kashmir.new(kashmir.taglib[template.strip], kashmir.escaper).code end end end