#!/usr/local/bin/ruby

# ruby-tmpl: Templating ala (mod_)ruby

#  Copyright 2001
#  	Sean Chittenden <sean@chittenden.org>.  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions
#  are met:
#  1. Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#  3. All advertising materials mentioning features or use of this software
#     must display the following acknowledgment:
#
#    This product includes software developed by Sean Chittenden
#    <sean@chittenden.org> and ruby-tmpl's contributors.
#
#  4. Neither the name of the software nor the names of its contributors
#     may be used to endorse or promote products derived from this software
#     without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
#  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
#  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
#  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
#  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#  SUCH DAMAGE.
#
# Please see the LICENSE file for more information regarding the use
# and distribution of this software.
#
# $Id: ruby-tmpl.rb,v 1.40 2002/02/21 19:12:03 thetitan Exp $

class Template
  attr_accessor :munge_ml_comments, :munge_whitespace, :max_num_iteration,
    :out_file, :out_file_mode, :session_param, :use_mod_ruby
  attr_reader :docroot, :file, :path, :is_compiled, :mod_ruby, :r,
    :session, :time_compile


  public

  def file= (file_name)
    if file_name != @file
      reset_output
      @file = file_name
    end
  end


  def munge_output
    return(@want_munged)
  end


  def munge_output= (val)
    raise RuntimeError, 'Content already munged' if (@is_munged)
    @want_munged = val
  end


  def path= (path)
    self.path_clear()
    self.path_append(path)
  end


  def path_append (path)
    path.to_a.each { |dir| @path.push(dir.split(/:/)) }
    @path.flatten!
  end


  def path_clear ()
    @path = ['.']
  end


  def print ()
    compile unless (@is_compiled)
    munge unless (@is_munged)

    if @use_mod_ruby
      @r.headers_out.set('Content-Length', size_of.to_s)
      @r.send_http_header
      out(@output)
    else 
      out(@output)
    end
  end


  def out (*args)
    if @use_mod_ruby
      @r.print(args.join(''))
    else
      if @out_file.length > 0
        of = File.open(@out_file, @out_file_mode)
        of.print(args.join(''))
        of.close
      else
        Kernel.print(args.join(''))
      end
    end
  end


  def session= (val)
    raise RuntimeError, 'Session already set' if (@session && val != @session)
    @session = val
  end


  def set (key, val)
    ns, key = key.split(':', 2)
    if !key
      key = ns
      ns = ''
    end

    @blocks[ns] = {} unless @blocks.has_key?(ns)
    @blocks[ns][key] = val
    return([key, ns])
  end


  def size_of ()
    compile unless (@is_compiled)
    munge unless (@is_munged)
    return(@output.length)
  end


  def to_s ()
    compile unless (@is_compiled)
    munge unless (@is_munged)
    return(@output.dup)
  end


  private

  def initialize
    @blocks = {}
    @file = ''
    @is_compiled = false
    @is_munged = false
    @max_num_iteration = 1000
    @mod_ruby = false
    @multiline_tags = ['if', 'unless']
    @munge_whitespace = true
    @munge_ml_comments = false
    @out_file = ''
    @out_file_mode = 'w'
    @output = ''
    @path = ['.']
    @session = nil
    @session_param = 'session'
    @session_pattern_a = '(<[Aa].*?[Hh][Rr][Ee][Ff]\s*=\s*)(([\'\"])((?:[^#]|[Mm][Aa][Ii][Ll][Tt][Oo]).*?[^\?]*?){0,1}(\?.*?){0,1}\3)(.*?>)'
    @session_pattern_form = '(<[Ff][Oo][Rr][Mm].*?[Aa][Cc][Tt][Ii][Oo][Nn]\s*=\s*)(([\'"])([^\?]*?){0,1}(\?.*?){0,1}\3)(.*?>)'
    @session_pattern_img = '(<[Ii][Mm][Gg].*?[Ss][Rr][Cc]\s*=\s*)(([\'"])([^\?]*?){0,1}(\?.*?){0,1}\3)(.*?>)'
    @use_mod_ruby = false
    @tag_pattern = '\<\?tmpl\.([^\s\?]*)\s*(.*?)\s*\?\>'
    @time_compile = Time.at(0)
    @want_munged = true

    if ENV.has_key?('MOD_RUBY')
      @mod_ruby = true
      @use_mod_ruby = true
      @r = Apache.request
      @r.content_type = 'text/html'
      @docroot = r.server.document_root
    elsif ENV.has_key?('DOCUMENT_ROOT')
      @docroot = ENV['DOCUMENT_ROOT']
    elsif ENV.has_key?('PWD')
      @docroot = ENV['PWD']
    else
      @docroot = nil
    end
  end # def initialize()


  def compile ()
    return(false) if (@is_compiled)
    t = Time.now
    tmpl = parse_string(tmpl_file({'name' => @file}))

    @output <<= tmpl
    @is_compiled = true
    @time_compile = Time.now() - t
  end


  def escape_html (val)
    val.gsub!(/&/, '&amp;')
    val.gsub!(/\"/, '&quot;')
    val.gsub!(/</, '&lt;')
    val.gsub!(/>/, '&gt;')

    return(val)
  end


  def find_end_tag (tmpl)
    unparsed_tmpl = String.new(tmpl)
    nest_level = 0
    end_char = 0
    end_tag = 0
    tag_re = Regexp.new(@tag_pattern)
    md = tag_re.match(unparsed_tmpl)
    unparsed_tmpl.slice!(0,md.end(0))
    first_tag_len = md.end(0)

    while 1
      md = tag_re.match(unparsed_tmpl)
      tag = md.to_a[0]
      cmd = md.to_a[1]

      if tag

	case cmd
	when 'else' && nest_level == 0
	  # do something
	when 'end'
	  if nest_level == 0
	    unparsed_tmpl.slice!(-tag.length, tag.length)
	    end_tag = tag
	    end_char += md.begin(0)
	    break
	  else
	    end_char += md.end(0)
	    unparsed_tmpl.slice!(0, md.end(0))
	    nest_level -= 1
	  end
	else
	  end_char += md.end(0)
	  unparsed_tmpl.slice!(0,md.end(0))
	  nest_level += 1 if @multiline_tags.include?(cmd)
	end # case cmd

      else
	break
      end
    end # while nested_level

    if nest_level > 0
      raise(RuntimeError, 'Invalid template: multiline tags do not match up')
    end

    return([tmpl.slice(first_tag_len, end_char), end_tag])
  end # def find_end_tag()


  def munge ()
    return(false) if (!@want_munged || @is_munged)
    compile unless (@is_compiled)
    if @munge_whitespace
      @output.gsub!(/\>\s+\</, '><')
      @output.gsub!(/\n\s*/, ' ')
    end

    if @munge_ml_comments
      @output.gsub!(/\<\!\-\-.*?\-\-\>/, '')
    end

    @munged = true
  end


  def parse_string (unparsed_tmpl, parent_args = {})
    tmpl = ''
    args = {}

    tag_re = Regexp.new(@tag_pattern)
    loop_count = 0
    while md = tag_re.match(unparsed_tmpl)
      if loop_count > @max_num_iteration
	raise(RuntimeError, 'Too many iterations') 
      else 
	loop_count += 1
      end

      tmpl <<= md.pre_match
      unparsed_tmpl = md.to_a[0] + md.post_match

      tag = md.to_a[0]
      cmd = md.to_a[1]

      args = {}
      args.update(parent_args)
      ['name'].each { |safe| args.delete(safe) }
      args.update(parse_tag_args(md.to_a[2]))

      val = ''

      case cmd
      when 'docroot_include'
	val = tmpl_docroot_include(args)
      when 'env'
	val = tmpl_env(args)
      when 'eval'
	val = tmpl_eval(args)
      when 'file'
	val = tmpl_file(args)
      when 'http_headers_in'
	val = tmpl_http_headers_in(args)
      when 'http_headers_out'
	val = tmpl_http_headers_out(args)
      when 'if'
	tag_info = find_end_tag(unparsed_tmpl)
	val = tmpl_if(tag_info[0], args)
	tag <<= tag_info.to_s
      when 'path'
	val = tmpl_path(args)
      when 'session'
	val = tmpl_session(args)
      when 'style'
	val = tmpl_style(args)
      when 'unless'
	tag_info = find_end_tag(unparsed_tmpl)
	val = tmpl_unless(tag_info[0], args)
	tag <<= tag_info.to_s
      when 'var'
	val = tmpl_var(args)
      else
	val = "<error>Invalid command: \"#{cmd}\"</error>"
      end

      if args.has_key?('escape')
	if args['escape'] == 'html'
	  val = escape_html(val)
	end
      end

      unparsed_tmpl.gsub!(/#{Regexp.escape(tag)}/, val)
    end

    tmpl <<= unparsed_tmpl

    sessionize_html(tmpl) if @session

    return(tmpl)
  end # def parse_string()


  def parse_tag_args (arg_str)
    args = {}
    arg_re = Regexp.new('\s*([^\s\=\"\'\>]*?)\s*\=\s*([\"\'])([^\2]*?)\2\s*')

    while md = arg_re.match(arg_str)
      args[md.to_a[1]] = md.to_a[3]
      arg_str = md.post_match
    end

    return(args)
  end


  def reset_output ()
    @is_compiled = false
    @is_munged = false
    @output = ''
    @time_compile = Time.at(0)
  end


  def sessionize_html (tmpl)
    sessionize_tags(tmpl, @session_pattern_a)
    sessionize_tags(tmpl, @session_pattern_form)
    sessionize_tags(tmpl, @session_pattern_img)
  end


  def sessionize_tags(tmpl, pattern)
    tmpl_orig = String.new(tmpl)
    session_regexp = Regexp.new(pattern)

    while (md = session_regexp.match(tmpl_orig)) do
      tag = md.to_a[1] + md.to_a[3] + md.to_a[4]

      if md.to_a[4].index('mailto') == 0
	tag = md.to_a[0]
      elsif md.to_a[5]
	if md.to_a[5].index(/[\?\+]#{@session_param}=/)
	  tag <<= md.to_a[5] + md.to_a[3] + md.to_a[6]
	else
	  tag <<= md.to_a[5] + '+' + @session_param + '=' + @session + md.to_a[3] + md.to_a[6]
	end
      else
	tag <<= '?' + @session_param + '=' + @session + md.to_a[3] + md.to_a[6]
      end

      tmpl.gsub!(/#{Regexp.escape(md.to_a[0])}/, tag)
      tmpl_orig = md.post_match
    end # while loop

    return(tmpl)
  end # def sessionize_tags()


  def test_block (block_name, args)
    ns = ''
    ns = args['ns'] if args.has_key?(args['ns'])

    if @blocks[ns].has_key?(block_name)
      return(true)
    elsif @blocks[''].has_key?(block_name)
      return(true)
    else
      return(false)
    end
  end


  ##################
  # Begin tag libs #
  ##################

  # <?tmpl.docroot_include ?>
  def tmpl_docroot_include (args)
    val = ''

    return('<error>tmpl.docroot_include: Unable to determine the root of the request</error>') unless @docroot

    filename = (@mod_ruby ? @r.filename : $0)

    if args.has_key?('order') && (args['order'] == 'prepend' || args['order'] == 'append')
      order = args['order']
    else
      order = 'append'
    end

    spacer = (args.has_key?('spacer') ? args['spacer'] : ' ')

    uri = filename.sub(/\A#{Regexp.escape(@docroot)}/, '')

    if (uri == filename)
      return('<error>tmpl.docroot_include: Unable to create a URI relative to the document root</error>')
    end

    dirs = uri.split(/\//)
    dirs.compact!
    dirs.shift     # Remove residual leading /
    dirs.pop       # Remove trailing filename

    args_root_orig = args['root']

    includes = []
    (0 .. dirs.nitems).each do |n|
      args['root'] = File.join(@docroot, dirs[n])

      if order == 'append'
	includes.unshift(tmpl_file(args))
      else
	includes.push(tmpl_file(args))
      end
	
      dirs.pop
    end

    val = includes.join(spacer)

    args['root'] = args_root_orig

    return(val.to_s)
  end # def tmpl_docroot_include ()


  # <?tmpl.env ?>
  def tmpl_env (args)
    val = ''

    if args.has_key?('name')
      if ENV.has_key?(args['name'])
	val = ENV[args['name']]
      elsif args.has_key?('default')
	val = args['default']
      else
	val = "<error>ENV[\'#{args['name']}\'] has no value</error>"
      end
    else
      val = "<error>ENV[\'#{args['name']}\'] has no value</error>"
    end

    return(val.to_s)
  end # def tmpl_env ()


  # <?tmpl.eval ?>
  def tmpl_eval (args)
    val = ''

    if args.has_key?('code')
      begin
	val = eval(args['code'].to_s)
      rescue ScriptError
	if args.has_key?('error_msg')
	  val = args['error_msg']
	else
	  val = "<error>eval of \"#{args['code']}\" failed</error>"
	end
      end
    end

    return(val.to_s)
  end # tmpl_eval ()
  

  # <?tmpl.file ?>
  def tmpl_file (args)
    path = []
    val = ''

    if args.has_key?('name')
      include_file = ''

      if args.has_key?('root')
	if args['root'] == 'docroot'
	  path.unshift(@docroot)
	else 
	  path.unshift(args['root'])
	end
      else
	path = @path
      end

      dir = ''
      path.each do |d|
	begin
	  file = File.join(d, args['name']).untaint
	  if File.stat(file).file? and File.stat(file).readable?
	    dir = d
	    break
	  end
	rescue Errno::ENOENT
	  next
	end
      end
      
      include_file = File.join(dir, args['name'])

      begin
        file = File.open(include_file.untaint)
        include = file.readlines
        val = include.join('')
        file.close()
      rescue Errno::ENOENT
        if args.has_key?('error_msg')
          if args['error_msg'] == 'yes'
            val = "<error>File \"#{args['name']}\" does not exist in the path \"#{path.join(':')}\"</error>"
          else
            val = args['error_msg']
          end
        else
          val = "<error>File \"#{args['name']}\" does not exist in the path \"#{path.join(':')}\"</error>"
        end
      end # begin
    end

    return(parse_string(val.to_s, args))
  end # def tmpl_file ()


  # <?tmpl.http_headers_in ?>
  def tmpl_http_headers_in (args)
    val = ''
    return(val) unless @mod_ruby

    if args.has_key?('name')
      if @r.headers_in.get(args['name'])
	val = @r.headers_in.get(args['name'])
      elsif args.has_key?('default')
	val = args['default']
      else
	val = "<error>Incoming HTTP Header \"#{args['name']}\" has no value</error>"
      end
    else
      val = "<error>Incoming HTTP Header \"#{args['name']}\" has no value</error>"
    end

    return(val.to_s)
  end # def tmpl_http_headers_in ()


  # <?tmpl.http_headers_out ?>
  def tmpl_http_headers_out (args)
    val = ''
    return(val) unless @mod_ruby

    if args.has_key?('name')
      if @r.headers_out.get(args['name'])
	val = @r.headers_out.get(args['name'])
      elsif args.has_key?('default')
	val = args['default']
      else
	val = "<error>Outgoing HTTP Header \"#{args['name']}\" has no value</error>"
      end
    else
      val = "<error>Outgoing HTTP Header \"#{args['name']}\" has no value</error>"
    end

    return(val.to_s)
  end # def tmpl_http_headers_out ()


  # <?tmpl.if ?> ... <?tmpl.end ?>
  def tmpl_if (unparsed_tmpl, args)
    val = ''
    type = args['type']
    condition_true = false
    type = (args.has_key?('type') ? args['type'] : 'block')

    if args.has_key?('name')
      case type
      when 'block'
	condition_true = test_block(args['name'], args)
      else
	val <<= "<error>Invalid if type type: \"#{args['type']}\"</error>"
      end
    end

    return(condition_true ? parse_string(unparsed_tmpl, args) : val)
  end # def tmpl_if ()


  # <?tmpl.loop ?> .. <?tmpl.end ?>
  # <?tmpl.loop name="stats" ?> ... <?tmpl.var name="stat" ?> ... <?tmpl.end ?>
  # tmpl.loop('stats',['stat1','stat2','stat3','stat4'])
  def tmpl_loop (unparsed_tmpl, args)
  end # def tmpl_loop ()


  # <?tmpl.path ?>
  def tmpl_path (args)
    val = ''
    spacer = ':'

    if args.has_key?('path')
      if (!args.has_key?('clear')) or (args.has_key?('clear') and args['clear'] != 'no')
	path_append(args['path'])
      else
	path=(args['path'])
      end
    end

    if args.has_key?('show') and args['show'] != 'no'
      if args.has_key?('spacer')
	spacer = args['spacer']
      end

      val = @path.join(spacer)
    end

    return(val.to_s)
  end # def tmpl_path ()


  # <?tmpl.session ?>
  def tmpl_session (args)
    if args.has_key?('name')
      self.session=(args['name'])
    end

    if (!args.has_key?('show')) || (args.has_key?('show') && args['show'] != 'no')
      val = @session
    else
      val = ''
    end

    return((val ? val.to_s : ''))
  end # def tmpl_session ()


  # <?tmpl.style ?>
  def tmpl_style (args)
    val = ''
    spacer = ':'

    if args.has_key?('name')
      file=(args['name'])
    end

    if args.has_key?('show') and args['show'] != 'no'
      val = @file
    end

    return(val.to_s)
  end # def tmpl_style ()


  # <?tmpl.unless ?> ... <?tmpl.end ?>
  def tmpl_unless (unparsed_tmpl, args)
    val = ''
    type = args['type']
    condition_true = false
    type = (args.has_key?('type') ? args['type'] : 'block')

    if args.has_key?('name')
      case type
      when 'block'
	condition_true = test_block(args['name'], args)
      else
	val <<= "<error>Invalid unless type type: \"#{args['type']}\"</error>"
      end
    end

    return(condition_true ? val : parse_string(unparsed_tmpl, args))
  end # def tmpl_unless ()


  # <?tmpl.var ?>
  def tmpl_var (args)
    val = ''
    ns = ''

    if args.has_key?('name')
      ns = args['ns'] if args.has_key?('ns')

      if @blocks[ns].has_key?(args['name'])
	val = @blocks[ns][args['name']]
      elsif @blocks[''].has_key?(args['name'])
	val = @blocks[''][args['name']]
      elsif args.has_key?('default')
	val = args['default']
      else
	val = "<error>variable \"#{args['name']}\" has no value</error>"
      end
    end

    return(val.to_s)
  end # def tmpl_var ()

end # class Template
