class HTree::TemplateCompiler

Constants

ID_PAT
IGNORABLE_ELEMENTS
NAME_FARGS_PAT
WhiteSpacePreservingElements

Public Class Methods

new() click to toggle source
# File htree/template.rb, line 443
def initialize
  @gensym_id = 0
end

Public Instance Methods

check_syntax(code) click to toggle source
# File htree/template.rb, line 709
def check_syntax(code)
  unless valid_syntax?(code)
    raise HTree::Error, "invalid ruby code: #{code}"
  end
end
compile_body(outvar, contextvar, node, is_toplevel, local_templates={}) click to toggle source
# File htree/template.rb, line 614
def compile_body(outvar, contextvar, node, is_toplevel, local_templates={})
  if node.elem? && IGNORABLE_ELEMENTS[node.name] && node.attributes.empty?
    node = TemplateNode.new(node.children)
  else
    node = TemplateNode.new(node)
  end
  generate_logic_node([:content], node, local_templates).generate_xml_output_code(outvar, contextvar)
end
compile_call(ignore_tag, spec) click to toggle source
# File htree/template.rb, line 760
def compile_call(ignore_tag, spec)
  # spec : [recv.]meth[(args)]
  spec = spec.strip
  spec, args = split_args(spec)
  unless /#{ID_PAT}\z/o =~ spec
    raise HTree::Error, "invalid _call: #{spec}"
  end
  meth = $&
  spec = $`
  if /\A\s*\z/ =~ spec
    recv = nil
  elsif /\A\s*(.*)\.\z/ =~ spec
    recv = $1
  else
    raise HTree::Error, "invalid _call: #{spec}"
  end
  if recv
    check_syntax(recv)
    check_syntax("#{recv}.#{meth}(#{args})")
  end
  check_syntax("#{meth}(#{args})")
  [:call, recv, meth, args]
end
compile_dynamic_text(ignore_tag, expr) click to toggle source
# File htree/template.rb, line 715
def compile_dynamic_text(ignore_tag, expr)
  check_syntax(expr)
  logic = [:text, expr]
  logic = [:tag, logic] unless ignore_tag
  logic
end
compile_dynamic_tree(ignore_tag, expr) click to toggle source
# File htree/template.rb, line 722
def compile_dynamic_tree(ignore_tag, expr)
  check_syntax(expr)
  logic = [:tree, expr]
  logic = [:tag, logic] unless ignore_tag
  logic
end
compile_global_template(name_fargs, node) click to toggle source
# File htree/template.rb, line 568
  def compile_global_template(name_fargs, node)
    unless /\A#{NAME_FARGS_PAT}\z/o =~ name_fargs
      raise HTree::Error, "invalid template declaration: #{name_fargs}"
    end
    name = $1
    fargs = $2 ? $2.scan(ID_PAT) : []

    outvar = gensym('out')
    contextvar = gensym('top_context')
    args2 = [outvar, contextvar, *fargs]

    <<"End"
def #{name}(#{fargs.join(',')})
HTree.parse(_xml_#{name}(#{fargs.join(',')}))
end
def _xml_#{name}(#{fargs.join(',')})
#{outvar} = HTree::Encoder.new(HTree::Encoder.internal_charset)
#{contextvar} = HTree::DefaultContext
_ht_#{name}(#{args2.join(',')})
#{outvar}.finish
end
def _ht_#{name}(#{args2.join(',')})
#{compile_body(outvar, contextvar, node, false)}\
end
public :_ht_#{name}
End
  end
compile_if(ignore_tag, expr, else_call) click to toggle source
# File htree/template.rb, line 729
def compile_if(ignore_tag, expr, else_call)
  check_syntax(expr)
  then_logic = [:content]
  unless ignore_tag
    then_logic = [:tag, then_logic]
  end
  else_logic = nil
  if else_call
    else_logic = compile_call(true, else_call)
  end
  [:if, expr, then_logic, else_logic]
end
compile_iter(ignore_tag, spec) click to toggle source
# File htree/template.rb, line 784
def compile_iter(ignore_tag, spec)
  # spec: <n _iter="expr.meth[(args)]//fargs" >...</n>
  spec = spec.strip
  unless %r{\s*//\s*(#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)?\z}o =~ spec
    raise HTree::Error, "invalid block arguments for _iter: #{spec}"
  end
  call = $`.strip
  fargs = $1 ? $1.strip : ''
  check_syntax("#{call} {|#{fargs}| }")
  logic = [:content]
  unless ignore_tag
    logic = [:tag, logic]
  end
  [:iter, call, fargs, logic]
end
compile_iter_content(ignore_tag, spec) click to toggle source
# File htree/template.rb, line 800
def compile_iter_content(ignore_tag, spec)
  # spec: <n _iter_content="expr.meth[(args)]//fargs" >...</n>
  spec = spec.strip
  unless %r{\s*//\s*(#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)?\z}o =~ spec
    raise HTree::Error, "invalid block arguments for _iter: #{spec}"
  end
  call = $`.strip
  fargs = $1 ? $1.strip : ''
  check_syntax("#{call} {|#{fargs}| }")
  logic = [:content]
  logic = [:iter, call, fargs, logic]
  unless ignore_tag
    logic = [:tag, logic]
  end
  logic
end
compile_local_template(name_fargs, node, local_templates) click to toggle source
# File htree/template.rb, line 596
  def compile_local_template(name_fargs, node, local_templates)
    unless /\A#{NAME_FARGS_PAT}\z/o =~ name_fargs
      raise HTree::Error, "invalid template declaration: #{name_fargs}"
    end
    name = $1
    fargs = $2 ? $2.scan(ID_PAT) : []

    outvar = gensym('out')
    contextvar = gensym('top_context')
    args2 = [outvar, contextvar, *fargs]

    <<"End"
#{name} = lambda {|#{args2.join(',')}|
#{compile_body(outvar, contextvar, node, false, local_templates)}\
}
End
  end
compile_node(node, local_templates) click to toggle source
# File htree/template.rb, line 623
def compile_node(node, local_templates)
  case node
  when HTree::Doc
    TemplateNode.new(node.children.map {|n| compile_node(n, local_templates) })
  when HTree::Elem
    ht_attrs = node.attributes.find_all {|name, text| template_attribute? name }
    ht_attrs = ht_attrs.sort_by {|htname, text| htname.universal_name }
    ignore_tag = false
    unless ht_attrs.empty?
      attr_mod = {}
      ht_attrs.each {|htname, text|
        attr_mod[htname] = nil
        if /\A_attr_/ =~ htname.local_name
          attr_mod[TemplateAttrName.new(htname.namespace_prefix, htname.namespace_uri, $')] = text
        end
      }
      ht_attrs.reject! {|htname, text| /\A_attr_/ =~ htname.local_name }
      node = node.subst_subnode(attr_mod)
      ignore_tag = IGNORABLE_ELEMENTS[node.name] && node.attributes.empty?
    end
    ht_names = ht_attrs.map {|htname, text| htname.universal_name }
    ht_vals =  ht_attrs.map {|htname, text| text.to_s }
    case ht_names
    when []
      generate_logic_node([:tag, [:content]], node, local_templates)
    when ['_text'] # <n _text="expr" /> or <n _text>expr</n>
      if ht_vals[0] != '_text' # xxx: attribute value is really omitted?
        expr = ht_vals[0]
      else
        children = node.children
        if children.length != 1
          raise HTree::Error, "_text expression has #{children.length} nodes"
        end
        if !children[0].text?
          raise HTree::Error, "_text expression is not text: #{children[0].class}"
        end
        expr = children[0].to_s
      end
      if ignore_tag && /\A\s*'((?:[^'\]|\.)*)'\s*\z/m =~ expr
        # if expr is just a constant string literal, use it as a literal text.
        # This saves dynamic evaluation of <span _text="' '"/> 
        # xxx: handle "..." as well if it has no #{}.
        HTree::Text.new($1.gsub(/\(.)/m, '\1'))
      else
        generate_logic_node(compile_dynamic_text(ignore_tag, expr), node, local_templates)
      end
    when ['_tree'] # <n _tree="expr" /> or <n _tree>expr</n>
      if ht_vals[0] != '_tree' # xxx: attribute value is really omitted?
        expr = ht_vals[0]
      else
        children = node.children
        if children.length != 1
          raise HTree::Error, "_tree expression has #{children.length} nodes"
        end
        if !children[0].text?
          raise HTree::Error, "_tree expression is not text: #{children[0].class}"
        end
        expr = children[0].to_s
      end
      generate_logic_node(compile_dynamic_tree(ignore_tag, expr), node, local_templates)
    when ['_if'] # <n _if="expr" >...</n>
      generate_logic_node(compile_if(ignore_tag, ht_vals[0], nil), node, local_templates)
    when ['_else', '_if'] # <n _if="expr" _else="expr.meth(args)" >...</n>
      generate_logic_node(compile_if(ignore_tag, ht_vals[1], ht_vals[0]), node, local_templates)
    when ['_call'] # <n _call="recv.meth(args)" />
      generate_logic_node(compile_call(ignore_tag, ht_vals[0]), node, local_templates)
    when ['_iter'] # <n _iter="expr.meth(args)//fargs" >...</n>
      generate_logic_node(compile_iter(ignore_tag, ht_vals[0]), node, local_templates)
    when ['_iter_content'] # <n _iter_content="expr.meth(args)//fargs" >...</n>
      generate_logic_node(compile_iter_content(ignore_tag, ht_vals[0]), node, local_templates)
    else
      raise HTree::Error, "unexpected template attributes: #{ht_attrs.inspect}"
    end
  else
    return node
  end
end
compile_template(src) click to toggle source
# File htree/template.rb, line 508
  def compile_template(src)
    srcdoc = parse_template(src)
    templates = []
    extract_templates(srcdoc, templates, true)
    methods = []
    templates.each {|name_args, node|
      methods << compile_global_template(name_args, node)
    }
    <<"End"
require 'htree/encoder'
require 'htree/context'
Module.new.module_eval <<'EE'
module_function
#{methods.join('').chomp}
self
EE
End
  end
expand_template(template, out, encoding, binding) click to toggle source
# File htree/template.rb, line 490
  def expand_template(template, out, encoding, binding)
    template = parse_template(template)
    is_html = template_is_html(template)
    outvar = gensym('out')
    contextvar = gensym('top_context')
    code = ''
    code << "#{outvar} = HTree::Encoder.new(#{encoding.dump})\n"
    code << "#{outvar}.html_output = true\n" if is_html
    code << "#{contextvar} = #{is_html ? "HTree::HTMLContext" : "HTree::DefaultContext"}\n"
    code << compile_body(outvar, contextvar, template, false)
    code << "[#{outvar}.#{is_html ? "finish" : "finish_with_xmldecl"}, #{outvar}.minimal_charset]\n"
#puts code; STDOUT.flush
    result, minimal_charset = eval(code, binding, "(eval:#{__FILE__}:#{__LINE__})")
    out.charset = minimal_charset if out.respond_to? :charset=
    out << result
    out
  end
extract_templates(node, templates, is_toplevel) click to toggle source
# File htree/template.rb, line 531
def extract_templates(node, templates, is_toplevel)
  case node
  when HTree::Doc
    subst = {}
    node.children.each_with_index {|n, i|
      subst[i] = extract_templates(n, templates, is_toplevel)
    }
    node.subst_subnode(subst)
  when HTree::Elem
    ht_attrs, = node.attributes.partition {|name, text| template_attribute? name }
    if ht_attrs.empty?
      subst = {}
      node.children.each_with_index {|n, i|
        subst[i] = extract_templates(n, templates, is_toplevel)
      }
      node.subst_subnode(subst)
    else
      ht_attrs.each {|htname, text|
        if htname.universal_name == '_template'
          name_fargs = text.to_s
          templates << [name_fargs, node.subst_subnode('_template' => nil)]
          return nil
        end
      }
      if is_toplevel
        raise HTree::Error, "unexpected template attributes in toplevel: #{ht_attrs.inspect}"
      else
        node
      end
    end
  else
    node
  end
end
generate_logic_node(logic, node, local_templates) click to toggle source
# File htree/template.rb, line 817
def generate_logic_node(logic, node, local_templates)
  # logic ::= [:if, expr, then_logic, else_logic]
  #         | [:iter, call, fargs, logic]
  #         | [:tag, logic]
  #         | [:text, expr]
  #         | [:tree, expr]
  #         | [:call, expr, meth, args]
  #         | [:content]
  #         | [:empty]
  case logic.first
  when :empty
    nil
  when :content
    subtemplates = []
    children = []
    node.children.each {|c|
      children << extract_templates(c, subtemplates, false)
    }
    if subtemplates.empty?
      TemplateNode.new(node.children.map {|n|
        compile_node(n, local_templates)
      })
    else
      local_templates = local_templates.dup
      decl = ''
      subtemplates.each {|sub_name_args, sub_node|
        sub_name = sub_name_args[ID_PAT]
        local_templates[sub_name] = sub_name
        decl << "#{sub_name} = "
      }
      decl << "nil\n"
      defs = []
      subtemplates.each {|sub_name_args, sub_node|
        defs << lambda {|out, context|
          out.output_logic_line compile_local_template(sub_name_args, sub_node, local_templates)
        }
      }
      TemplateNode.new(
        lambda {|out, context| out.output_logic_line decl },
        defs,
        children.map {|n| compile_node(n, local_templates) }
      )
    end
  when :text
    _, expr = logic
    TemplateNode.new(lambda {|out, context| out.output_dynamic_text expr })
  when :tree
    _, expr = logic
    TemplateNode.new(lambda {|out, context| out.output_dynamic_tree expr, make_context_expr(out, context) })
  when :tag
    _, rest_logic = logic
    if rest_logic == [:content] && node.empty_element?
      node
    else
      subst = {}
      node.children.each_index {|i| subst[i] = nil }
      subst[0] = TemplateNode.new(generate_logic_node(rest_logic, node, local_templates))
      node.subst_subnode(subst)
    end
  when :if
    _, expr, then_logic, else_logic = logic
    children = [
      lambda {|out, context| out.output_logic_line "if (#{expr})" },
      generate_logic_node(then_logic, node, local_templates)
    ]
    if else_logic
      children.concat [
        lambda {|out, context| out.output_logic_line "else" },
        generate_logic_node(else_logic, node, local_templates)
      ]
    end
    children <<
      lambda {|out, context| out.output_logic_line "end" }
    TemplateNode.new(*children)
  when :iter
    _, call, fargs, rest_logic = logic
    TemplateNode.new(
      lambda {|out, context| out.output_logic_line "#{call} {|#{fargs}|" },
      generate_logic_node(rest_logic, node, local_templates),
      lambda {|out, context| out.output_logic_line "}" }
    )
  when :call
    _, recv, meth, args = logic
    TemplateNode.new(
      lambda {|out, context|
        as = [out.outvar, ", ", make_context_expr(out, context)]
        unless args.empty?
          as << ", " << args
        end
        if recv
          out.output_logic_line "(#{recv})._ht_#{meth}(#{as.join('')})"
        elsif local_templates.include? meth
          out.output_logic_line "#{meth}.call(#{as.join('')})"
        else
          out.output_logic_line "_ht_#{meth}(#{as.join('')})"
        end
      }
    )
  else
    raise Exception, "[bug] invalid logic: #{logic.inspect}"
  end
end
gensym(suffix='') click to toggle source
# File htree/template.rb, line 447
def gensym(suffix='')
  @gensym_id += 1
  "g#{@gensym_id}#{suffix}"
end
make_context_expr(out, context) click to toggle source
# File htree/template.rb, line 920
def make_context_expr(out, context)
  ns = context.namespaces.reject {|k, v| HTree::Context::DefaultNamespaces[k] == v }
  if ns.empty?
    result = out.contextvar
  else
    result = "#{out.contextvar}.subst_namespaces("
    sep = ''
    ns.each {|k, v|
      result << sep << (k ? k.dump : "nil") << '=>' << v.dump
      sep = ', '
    }
    result << ")"
  end
  result
end
parse_template(template) click to toggle source
# File htree/template.rb, line 452
def parse_template(template)
  strip_whitespaces(HTree.parse(template))
end
split_args(spec) click to toggle source
# File htree/template.rb, line 742
def split_args(spec)
  return spec, '' if /\)\z/ !~ spec
  i = spec.length - 1
  nest = 0
  begin
    raise HTree::Error, "unmatched paren: #{spec}" if i < 0
    case spec[i]
    when ?\)
      nest += 1
    when ?\(
      nest -= 1
    end
    i -= 1
  end while nest != 0
  i += 1
  return spec[0, i], spec[(i+1)...-1]
end
strip_whitespaces(template) click to toggle source
# File htree/template.rb, line 460
def strip_whitespaces(template)
  case template
  when HTree::Doc
    HTree::Doc.new(*template.children.map {|c| strip_whitespaces(c) }.compact)
  when HTree::Elem, HTree::Doc
    return template if WhiteSpacePreservingElements[template.name]
    subst = {}
    template.children.each_with_index {|c, i|
      subst[i] = strip_whitespaces(c)
    }
    template.subst_subnode(subst)
  when HTree::Text
    if /\A[ \t\r\n]*\z/ =~ template.rcdata
      nil
    else
      template
    end
  else
    template
  end
end
template_attribute?(name) click to toggle source
# File htree/template.rb, line 527
def template_attribute?(name)
  /\A_/ =~ name.local_name
end
template_is_html(template) click to toggle source
# File htree/template.rb, line 482
def template_is_html(template)
  template.each_child {|c|
    return false if c.xmldecl?
    return true if c.elem? && c.element_name.namespace_uri == 'http://www.w3.org/1999/xhtml'
  }
  false
end
valid_syntax?(code) click to toggle source
# File htree/template.rb, line 701
def valid_syntax?(code)
  begin
    eval("BEGIN {return true}\n#{code.untaint}")
  rescue SyntaxError
    raise SyntaxError, "invalid code: #{code}"
  end
end