# -*- coding: utf-8 -*- require 'html/template/internal' module HTML # :nodoc: module Template # :nodoc: module Internal # :nodoc: class State # :nodoc: # supress documentation end module_function def register_functions_impl(registory, func_spec, &block) # :nodoc: if block && func_spec.kind_of?(Symbol) registory[func_spec] = block elsif func_spec.kind_of? Hash unless func_spec.values.all?{|e| e.kind_of? Proc} raise ArgumentError, "functions must be kind_of Proc" end registory.update(func_spec) else raise ArgumentError, "first argument must be symbol or hash contains functions" end end end class Pro VERSION = "0.0.3" # :stopdoc: ASK_NAME_DEFAULT = 0 ASK_NAME_AS_IS = 1 ASK_NAME_LOWERCASE = 2 ASK_NAME_UPPERCASE = 4 ASK_NAME_MASK = ASK_NAME_AS_IS | ASK_NAME_LOWERCASE | ASK_NAME_UPPERCASE INPUTS = [:filename, :filehandle, :arrayref, :scalarref, :source] # :startdoc: @@func = { # note that length,defined,sin,cos,log,tan,... are built-in :sprintf => lambda { |*args| sprintf(*args) }, :substr => lambda { |str, *args| args.size == 2 ? str[*args] : str[args[0]..-1] }, :lc => lambda { |str| str.downcase }, :lcfirst => lambda { |str| str[0,1].downcase + str[1..-1] }, :uc => lambda { |str| str.upcase }, :ucfirst => lambda { |str| str.capitalize }, # :length => lambda { |str| str.length }, # :defined => lambda { |obj| obj.nil? }, # :abs => lambda { |num| num.abs }, # :hex => lambda { |obj| obj.to_s.hex }, # :oct => lambda { |obj| obj.to_s.oct }, :rand => lambda { |num| rand num }, :srand => lambda { |seed| srand seed }, } # Create a `Template' object. `Template' source is specified by several way. # Exactly one kind of source must be specified. # # * specify filename # # template = HTML::Template::Pro.new(:filename => 'foo.tmpl') # # * specify source as string # # template = HTLM::Template::Pro.new(:source => '<html><body><TMPL_VAR NAME="hello">!!</body></html>') # # * specify source by array of string # # template = HTLM::Template::Pro.new(:source => ['<html>', '<body><TMPL_VAR NAME="hello">!!</body>', '</html>']) # # * specify IO object # # template = HTLM::Template::Pro.new(:source => $stdin) # # === Other options # # ==== path # # Array of file path which include template files. # # Example: # # # find out tmpl/foo.tmpl # template = HTLM::Template::Pro.new(:filename => 'foo.tmpl', :path => ['tmpl']) # # ==== search_path_on_include # # Boolean that indicates whether we search directory specified # by `path' param when we meet the TMPL_INCLUDE tag. # # Example: # # # tmpl_includes/foo.tmpl is used. # template = HTLM::Template::Pro.new(:source => '<TMPL_INCLUDE NAME="foo.tmpl">', # :path => ['tmpl_includes'], # :search_path_on_include => true) # # ==== associate # # this option allows you to inherit the parameter values from # other objects. The only requirement for the other object is # that it have a "param()" method that works like HTML::Template::Pro#param. # # ==== case_sensitive # # if this options is true, template variable is treated as case sensitive. # # Example: # # template = HTML::Template::Pro.new(:source => '<TMPL_VAR NAME="FoO">', # :case_sensitive => true) # template.param(:foo => 100) # puts template.output # output empty string ( foo is not 'FoO' ) # # ==== loop_context_vars # # when this parameter is set to true (it is false by default) # five loop context variables are made available inside a loop: # __first__, __last__, __inner__, __odd__, __counter__. They # can be used with <TMPL_IF>, <TMPL_UNLESS> and <TMPL_ELSE> to # control how a loop is output. # # Example: # In your templates, # # <TMPL_LOOP NAME="loop"> # <TMPL_IF NAME="__first__">First!</TMPL_IF> # <TMPL_VAR NAME="__counter__"> # <TMPL_IF NAME="__last__">Last!</TMPL_IF> # </TMPL_LOOP> # # ==== no_includes # # set this option to true to disallow the <TMPL_INCLUDE> tag in the # template file. This can be used to make opening untrusted # templates *slightly* less dangerous. Defaults to false. # # ==== max_includes # # set this variable to determine the maximum depth that includes # can reach. Set to 10 by default. Including files to a depth # greater than this value causes an error message to be # displayed. Set to 0 to disable this protection. # # ==== global_vars # # normally variables declared outside a loop are not available # inside a loop. This option makes <TMPL_VAR>s like global # variables - they have unlimited scope. This option also # affects <TMPL_IF> and <TMPL_UNLESS>. # # ==== path_like_variable_scope # # this option switches on a Shigeki Morimoto extension to # HTML::Template::Pro that allows access to variables that are # outside the current loop scope using path-like expressions. # # Example: # # <TMPL_LOOP NAME=class> # <TMPL_LOOP NAME=person> # <TMPL_VAR NAME="../teacher_name"> <!-- access to class.teacher_name --> # <TMPL_VAR NAME="name"> # <TMPL_VAR NAME="/top_level_value"> <!-- access to top level value --> # <TMPL_VAR NAME="age"> # <TMPL_LOOP NAME="../../school"> <!-- enter loop before accessing its vars --> # <TMPL_VAR NAME="school_name"> <!-- access to [../../]school.school_name --> # </TMPL_LOOP> # </TMPL_LOOP> # </TMPL_LOOP> # # ==== filter # # By using this option, you can filter the source of template # before HTML::Template::Pro prcesses it. # # Example: # # myfilter = ->(source){ source.gsub(/Foo/, 'Bar') } # template = HTML::Template::Pro.new(:source => '<TMPL_VAR NAME="Foo">!!', # :filter => myfilter) # template.param(:Bar => 'hello') # puts template.output # displays 'hello!!' # # ==== default_escape # # Set this parameter to "HTML", "URL" or "JS" and # HTML::Template::Pro will apply the specified escaping to all # variables unless they declare a different escape in the # template. # def initialize(args={}) @options = default_options.merge(args) if args.keys.count(&INPUTS.method(:include?)) != 1 raise ArgumentError, "HTML::Template::Pro.new called with multiple (or no) template sources specified!" end @params = @options[:param_map] [:path, :associate, :filter].each do |opt| unless @options[opt].kind_of? Array @options[opt] = [ @options[opt] ] end end @options[:expr_func] = @@func.merge(@options[:functions] || {}) initialize_tmpl_source args if @scalarref and @options[:filter] @scalarref = call_filters @scalarref end @filtered_template = {} end # #param can be called in a number of ways. # # 1. call with no arguments. # # returns a list of parameters set after new. # # Example: # template = HTML::Template::Pro.new(:filename => 'foo.tmpl') # template.param(:foo => 10, :bar => 20) # params = template.param # returns [:foo, :bar] # # 2. pass one parameter name. # # returns the value of a parameter. # # Example: # template = HTML::Template::Pro.new(:filename => 'foo.tmpl') # template.param(:foo => 10, :bar => 20) # val = template.param(:foo) # now `val' equals 10. # # 3. pass hash. # # assign values to parameters. # # Example: # # template = HTML::Template::Pro.new(:source => <<'TMPL') # <TMPL_VAR NAME="foo"> # <TMPL_VAR NAME="bar"> # TMPL # template.param(:foo => 10, :bar => 20) # puts template.output # displays "10\n20\n" # # # you can specify the value by passing block. # # in this way, you must assign exactly one parameter. # template.param(:foo) { Time.now } # puts template.output # prints the time #output is executed. # # # Or you can use proc or lambda. # template.param(:foo => lambda { Time.now }, # :bar => lambda { rand(100) }) # def param(args=nil, &block) return @params.keys if args.nil? if !(args.kind_of? Hash) key = @options[:case_sensitive] ? args : args.downcase if block return @params[key] = block else return @params[key] || @params[args] end end merge_params(args) end # Clear the internal param hash. All the parameters set before # this call is reset. def clear_params @params.clear end # returns the final result of the template. If you want to print # out the result, you can do: # puts template.output # # When `output' is called each occurrence of <TMPL_VAR # NAME=name> is replaced with the value assigned to "name" via # "param()". If a named parameter is unset it is simply replaced # with ’’. <TMPL_LOOPS> are evaluated once per parameter set, # accumulating output on each pass. # # You may optionally supply a IO object to print: # File.open('result.html', 'w') do |file| # template.output(:print_to => file) # end def output(options={}) @options[:associate].reverse.each do |assoc| assoc.param.each do |key| param(key => assoc.param(key)) unless @params[key] end end if (options.include? :print_to) HTML::Template::Internal.exec_tmpl(self, options[:print_to]) else output_string = String.new HTML::Template::Internal.exec_tmpl(self, output_string) return output_string end end # <b>for perl compatibility</b> # # shortcut to HTML::Template::Pro.new(:filehandle => file) def self.new_filehandle(file) self.new(:filehandle => file) end # <b>for perl compatibility</b> # # shortcut to HTML::Template::Pro.new(:filename => filename) def self.new_file(filename) self.new(:filename => file) end # <b>for perl compatibility</b> # # shortcut to HTML::Template::Pro.new(:arrayref => lines) def self.new_array_ref(lines) self.new(:arrayref => lines) end # <b>for perl compatibility</b> # # shortcut to HTML::Template::Pro.new(:scalarref => source_string) def self.new_scalar_ref(source) self.new(:scalarref => source) end # register the function globally. See HTML::Template::Pro#register_function. # Functions registered by this class method is shared by all instances. def self.register_function(func_spec, &block) HTML::Template::Internal.register_functions_impl(@@func, func_spec, &block) end # define a new function that can be used in <tt><TMPL_VAR # EXPR=""></tt> tag. function is specified by block or lambda. # # Example: # template = HTML::Template::Pro.new(:source => '<TMPL_VAR EXPR="double(10)">') # template.register_function(:double) {|num| num * 2 } # # or template.register_function(:double => lambda {|num| num * 2 }) # puts template.output # displays `20' # # You can also register the functions by passing lambdas to new. # # Example: # template = HTML::Template::Pro.new(:filename => 'foo.tmpl', # :functions => { # :square => lambda {|x| x * x }, # }) # def register_function(func_spec, &block) HTML::Template::Internal.register_functions_impl(@options[:expr_func], func_spec, &block) end private def default_options return { :param_map => {}, :filter => [], :debug => 0, :max_includes => 10, :global_vars => false, :no_includes => false, :search_path_on_include => false, :loop_context_vars => false, :path_like_variable_scope => false, :path => [], :associate => [], :case_sensitive => false, :__strict_compatibility => true, :strict => false, :die_on_bad_params => false, } end def initialize_tmpl_source(args) if args.include? :filename @filename = args[:filename] @scalarref = nil return end @filename = nil if args.include? :source source = args[:source] @scalarref = case source when IO then source.read when Array then source.join('') when String then source else if source.respond_to? :to_str source.to_str else raise "unknown source type" end end elsif args.include? :scalarref @scalarref = args[:scalarref] elsif args.include? :arrayref @scalarref = args[:arrayref].join('') elsif args.include? :filehandle @scalarref = args[:filehandle].read end end def merge_params(params) unless @options[:case_sensitive] params = lowercase_keys params end @params.update(params) end def lowercase_keys(orighash) Hash[ orighash.map do |key, val| case val when Array then [key.downcase, val.map(&(method :lowercase_keys))] when Proc then [key.downcase, val] else [key.downcase, val] end end ] end def load_template(filepath) File.open(filepath, 'r') do |file| # filtered template is used in internal. we store it to `self' # to prevent gc. @filtered_template[filepath] = call_filters file.read end end def call_filters(template) @options[:filter].each do |filter| format, sub = case filter when Hash then filter.values_at(:format, :sub) when Proc then ['scalar', filter] else raise "bad value set for filter parameter - must be a Proc or a Hash object." end unless format and sub raise "bad value set for filter parameter - hash must contain \"format\" key and \"sub\" key." end unless format == 'array' or format == 'scalar' raise "bad value set for filter parameter - \"format\" must be either 'array' or 'scalar'" end unless sub.kind_of? Proc raise "bad value set for filter parameter - \"sub\" must be a code ref" end if format == 'scalar' template = sub.call(template) else template = sub.call(template.split("\n").map {|str| str + "\n"}).join('') end end return template end end # alias Expr = Pro end end