Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
962 lines (918 sloc) 30.4 KB
# = Template Engine
#
# The htree template engine converts HTML and some data to HTML or XML.
#
# == Template Method Summary
#
# - HTree.expand_template(<i>template_pathname</i>) -> $stdout
# - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>) -> $stdout
# - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>, <i>out</i>) -> <i>out</i>
# - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>, <i>out</i>, <i>encoding</i>) -> <i>out</i>
#
# - HTree.expand_template{<i>template_string</i>} -> $stdout
# - HTree.expand_template(<i>out</i>) {<i>template_string</i>} -> <i>out</i>
# - HTree.expand_template(<i>out</i>, <i>encoding</i>) {<i>template_string</i>} -> <i>out</i>
#
# - HTree.compile_template(<i>template_string</i>) -> Module
# - HTree{<i>template_string</i>} -> HTree::Doc
#
# Note that the following method, HTree(), is not a template method.
#
# - HTree(<i>html_string</i>) -> HTree::Doc
#
# == Template Directives.
#
# A template directive is described as a special HTML attribute which name
# begins with underscore.
#
# The template directives are listed as follows.
#
# - <elem \_attr_<i>name</i>="<i>expr</i>">content</elem>
# - <elem _text="<i>expr</i>">dummy-content</elem>
# - <elem _text><i>expr</i></elem>
# - <elem _tree="<i>expr</i>">dummy-content</elem>
# - <elem _tree><i>expr</i></elem>
# - <elem _if="<i>expr</i>" _else="<i>mod.name(args)</i>">then-content</elem>
# - <elem _iter="<i>expr.meth(args)//vars</i>">content</elem>
# - <elem _iter_content="<i>expr.meth(args)//vars</i>">content</elem>
# - <elem _call="<i>mod.name(args)</i>">dummy-content</elem>
# - <elem _template="<i>name(vars)</i>">body</elem>
#
# === Template Semantics
#
# - attribute substitution
# - <elem \_attr_<i>name</i>="<i>expr</i>">content</elem>
#
# \_attr_<i>name</i> is used for a dynamic attribute.
#
# <elem _attr_xxx="..."/>
# -> <elem xxx="..."/>
#
# It is expanded to <i>name</i>="content".
# The content is generated by evaluating _expr_.
# Usually you don't need to care escaping: &, <, > and " are automatically escaped.
# If you need to output character references,
# the value of _expr_ should be an object which have a +rcdata+ method such as an HTree::Text.
# If the value has a +rcdata+ method,
# it is called and the result is used as the content with escaping <, > and ".
#
# \_attr_<i>name</i> can be used multiple times in single element.
#
# - text substitution
# - <elem _text="<i>expr</i>">dummy-content</elem>
# - <elem _text><i>expr</i></elem>
#
# _text substitutes the content of the element by the string
# evaluated from _expr_.
# _expr_ is described in the attribute value or the content of the element.
#
# If a result of _expr_ have &, < and/or >, they are automatically escaped.
# If you need to output character references,
# the value of _expr_ should be an object which have a +rcdata+ method such as an HTree::Text.
# If the value has a +rcdata+ method,
# it is called and the result is used as the content with escaping < and >.
#
# If the element is span or div, and there is no other attributes,
# no tags are produced.
#
# <elem _text="...">dummy-content</elem>
# -> <elem>...</elem>
#
# - tree substitution
# - <elem _tree="<i>expr</i>">dummy-content</elem>
# - <elem _tree><i>expr</i></elem>
#
# _tree substitutes the content of the element by the htree object
# evaluated from _expr_.
# _expr_ is described in the attribute value or the content of the element.
#
# If the element is span or div, and there is no other attributes,
# no tags are produced.
#
# <elem _tree="...">dummy-content</elem>
# -> <elem>...</elem>
#
# - conditional
# - <elem _if="<i>expr</i>">then-content</elem>
# - <elem _if="<i>expr</i>" _else="<i>name(args)</i>">then-content</elem>
#
# _if is used for conditional.
#
# If <i>expr</i> is evaluated to true, it expands as follows
# regardless of existence of _else.
#
# <elem _if="<i>expr</i>">then-content</elem>
# -> <elem>then-content</elem>
#
# If <i>expr</i> is evaluated to false, it expands using _else.
# If _else is not given, it expands to empty.
# If _else is given, it expands as follows.
#
# <elem _if="<i>expr</i>" _else="<i>name(args)</i>">then-content</elem>
# -> <elem _call="<i>name(args)</i>">then-content</elem>
# -> see _call for further expansion.
#
# It is expanded to <elem>then-content</elem> if _expr_ is evaluated to
# a true value.
# Otherwise, it is replaced by other template specified by _else attribute.
# If _else attribute is not given, it just replaced by empty.
#
# - iteration
# - <elem _iter="<i>expr.meth(args)//vars</i>">content</elem>
# - <elem _iter_content="<i>expr.meth(args)//vars</i>">content</elem>
#
# _iter and _iter_content is used for iteration.
# _iter iterates the element itself but _iter_content iterates the content.
#
# <outer _iter="..."><inner/></outer>
# -> <outer><inner/></outer><outer><inner/></outer>...
#
# <outer _iter_content="..."><inner/></outer>
# -> <outer><inner/><inner/>...</outer>
#
# <i>expr.meth(args)</i> specifies iterator method call.
# It is actually called with a block.
# The block have block parameters <i>vars</i>.
# <i>vars</i> must be variables separated by comma.
#
# - template call
# - <elem _call="<i>name(args)</i>">dummy-content</elem>
# - <elem _call="<i>mod.name(args)</i>">dummy-content</elem>
#
# _call is used to expand a template function.
# The template function is defined by _template.
#
# <d _template="m">...</d>
# <c _call="m">...</c>
# -> <d>...</d>
#
# A local template can be called as follows:
#
# HTree.expand_template{<<'End'}
# <a _template=ruby_talk(num)
# _attr_href='"http://ruby-talk.org/#{num}"'
# >[ruby-talk:<span _text=num>nnn</span>]</a>
# Ruby 1.8.0 is released at <span _call=ruby_talk(77946) />.
# Ruby 1.8.1 is released at <span _call=ruby_talk(88814) />.
# End
#
# <i>mod</i> should be the result of HTree.compile_template.
#
# M = HTree.compile_template(<<'End')
# <a _template=ruby_talk(num)
# _attr_href='"http://ruby-talk.org/#{num}"'
# >[ruby-talk:<span _text=num>nnn</span>]</a>
# End
# HTree.expand_template{<<'End'}
# <html>
# Ruby 1.8.0 is released at <span _call=M.ruby_talk(77946) />.
# Ruby 1.8.1 is released at <span _call=M.ruby_talk(88814) />.
# </html>
# End
#
# The module can included.
# In such case, the template function can be called without <i>mod.</i>
# prefix.
#
# include HTree.compile_template(<<'End')
# <a _template=ruby_talk(num)
# _attr_href='"http://ruby-talk.org/#{num}"'
# >[ruby-talk:<span _text=num>nnn</span>]</a>
# End
# HTree.expand_template{<<'End'}
# <html>
# Ruby 1.8.0 is released at <span _call=ruby_talk(77946) />.
# Ruby 1.8.1 is released at <span _call=ruby_talk(88814) />.
# </html>
# End
#
# - template definition
# - <elem _template="<i>name(vars)</i>">body</elem>
#
# _template defines a template function which is usable by _call.
#
# When a template is compiled to a module by HTree.compile_template,
# the module have a module function for each template function
# defined by outermost _template attribute.
#
# === White Space Handling
#
# The htree template engine strips whitespace text nodes in a template
# except under HTML pre element.
#
# For example the white space text node between two spans in following template is stripped.
#
# <span _text="'a'"/> <span _text="'b'"/> -> "ab"
#
# Character entity references are not stripped.
#
# <span _text="'a'"/>&#32;<span _text="'b'"/> -> "a&#32;b"
#
# Text nodes generated by _text is not stripped.
#
# <span _text="'a'"/><span _text="' '"> </span><span _text="'b'"/> -> "a b"
#
# == HTML and XML
#
# The htree template engine outputs HTML or XML.
#
# If a template has no XML declaration and the top element is HTML,
# the result is HTML.
# Otherwise the result is XML.
#
# They differs as follows.
#
# - XML declaration is (re-)generated for XML.
# - empty elements ends with a slash for XML.
# - script and style element is escaped for XML.
#
# == Design Decision on Design/Logic Separation
#
# HTree template engine doesn't force you to separate design and logic.
# Any logic (Ruby code) can be embedded in design (HTML).
#
# However the template engine cares the separation by logic refactorings.
# The logic is easy to move between a template and an application.
# For example, following tangled template
#
# tmpl.html:
# <html>
# <head>
# <title _text="very-complex-ruby-code">dummy</title>
# </head>
# ...
# </html>
#
# app.rb:
# HTree.expand_template('tmpl.html', obj)
#
# can be refactored as follows.
#
# tmpl.html:
# <html>
# <head>
# <title _text="title">dummy</title>
# </head>
# ...
# </html>
#
# app.rb:
# def obj.title
# very-complex-ruby-code
# end
# HTree.expand_template('tmpl.html', obj)
#
# In general, any expression in a template can be refactored to an application
# by extracting it as a method.
# In JSP, this is difficult especially for a code fragment of an iteration.
#
# Also HTree encourages to separate business logic (Ruby code in an application)
# and presentation logic (Ruby code in a template).
# For example, presentation logic to color table rows stripe
# can be embedded in a template.
# It doesn't need to tangle an application.
#
module HTree
# :stopdoc:
EmptyBindingObject = Object.new
# :startdoc:
end
# :stopdoc:
def (HTree::EmptyBindingObject).empty_binding
binding
end
# :startdoc:
require 'htree/parse'
require 'htree/gencode'
require 'htree/equality'
require 'htree/traverse'
# call-seq:
# HTree.expand_template(template_pathname, obj=Object.new, out=$stdout, encoding=internal_encoding) -> out
# HTree.expand_template(out=$stdout, encoding=internal_encoding) { template_string } -> out
#
# <code>HTree.expand_template</code> expands a template.
#
# The arguments should be specified as follows.
# All argument except <i>pathname</i> are optional.
#
# - HTree.expand_template(<i>pathname</i>, <i>obj</i>, <i>out</i>, <i>encoding</i>) -> <i>out</i>
# - HTree.expand_template(<i>out</i>, <i>encoding</i>) {<i>template_string</i>} -> <i>out</i>
#
# The template is specified by a file or a string.
# If a block is not given, the first argument represent a template pathname.
# Otherwise, the block is yielded and its value is interpreted as a template
# string.
# So it can be called as follows in simplest case.
#
# - HTree.expand_template(<i>template_pathname</i>)
# - HTree.expand_template{<i>template_string</i>}
#
# Ruby expressions in the template file specified by _template_pathname_ are
# evaluated in the context of the optional second argument <i>obj</i> as follows.
# I.e. the pseudo variable self in the expressions is bound to <i>obj</i>.
#
# HTree.expand_template(template_pathname, obj)
#
# Ruby expressions in the template_string are evaluated
# in the context of the caller of HTree.expand_template.
# (binding information is specified by the block.)
# I.e. they can access local variables etc.
# We recommend to specify template_string as a literal string without
# interpolation because dynamically generated string may break lexical scope.
#
# HTree.expand_template has two more optional arguments:
# <i>out</i>, <i>encoding</i>.
#
# <i>out</i> specifies output target.
# It should have <tt><<</tt> method: IO and String for example.
# If it is not specified, $stdout is used.
# If it has a method <tt>charset=</tt>, it is called to set the minimal charset
# of the result before <tt><<</tt> is called.
#
# <i>encoding</i> specifies output character encoding.
# If it is not specified, internal encoding is used.
#
# HTree.expand_template returns <i>out</i> or $stdout if <i>out</i> is not
# specified.
#
def HTree.expand_template(*args, &block)
if block
template = block.call
binding = block
else
pathname = args.fetch(0) { raise ArgumentError, "pathname not given" }
args.shift
obj = args.fetch(0) { Object.new }
args.shift
if pathname.respond_to? :read
template = pathname.read.untaint
if template.respond_to? :charset
template = Iconv.conv(HTree::Encoder.internal_charset, template.charset, template)
end
else
template = File.read(pathname).untaint
end
Thread.current[:htree_expand_template_obj] = obj
binding = eval(<<-'End',
Thread.current[:htree_expand_template_obj].class.class_eval <<-'EE'
Thread.current[:htree_expand_template_obj].instance_eval { binding }
EE
End
HTree::EmptyBindingObject.empty_binding)
Thread.current[:htree_expand_template_obj] = nil
end
out = args.shift || $stdout
encoding = args.shift || HTree::Encoder.internal_charset
if !args.empty?
raise ArgumentError, "wrong number of arguments"
end
HTree::TemplateCompiler.new.expand_template(template, out, encoding, binding)
end
# call-seq:
# HTree(html_string) -> doc
# HTree{template_string} -> doc
#
# <code>HTree(<i>html_string</i>)</code> parses <i>html_string</i>.
# <code>HTree{<i>template_string</i>}</code> parses <i>template_string</i> and expand it as a template.
# Ruby expressions in <i>template_string</i> is evaluated in the scope of the caller.
#
# <code>HTree()</code> and <code>HTree{}</code> returns a tree as an instance of HTree::Doc.
def HTree(html_string=nil, &block)
if block_given?
raise ArgumentError, "both argument and block given." if html_string
template = block.call
HTree.parse(HTree::TemplateCompiler.new.expand_template(template, '', HTree::Encoder.internal_charset, block))
else
HTree.parse(html_string)
end
end
# call-seq:
# HTree.compile_template(template_string) -> module
#
# <code>HTree.compile_template(<i>template_string</i>)</code> compiles
# <i>template_string</i> as a template.
#
# HTree.compile_template returns a module.
# The module has module functions for each templates defined in
# <i>template_string</i>.
# The returned module can be used for +include+.
#
# M = HTree.compile_template(<<'End')
# <p _template=birthday(subj,t)>
# <span _text=subj />'s birthday is <span _text="t.strftime('%B %dth %Y')"/>.
# </p>
# End
# M.birthday('Ruby', Time.utc(1993, 2, 24)).display_xml
# # <p>Ruby's birthday is February 24th 1993.</p>
#
# The module function takes arguments specifies by a <code>_template</code>
# attribute and returns a tree represented as HTree::Node.
#
def HTree.compile_template(template_string)
code = HTree::TemplateCompiler.new.compile_template(template_string)
Thread.current[:htree_compile_template_code] = code
mod = eval(<<-'End',
eval(Thread.current[:htree_compile_template_code])
End
HTree::EmptyBindingObject.empty_binding)
Thread.current[:htree_compile_template_code] = nil
mod
end
# :stopdoc:
class HTree::TemplateCompiler
IGNORABLE_ELEMENTS = {
'span' => true,
'div' => true,
'{http://www.w3.org/1999/xhtml}span' => true,
'{http://www.w3.org/1999/xhtml}div' => true,
}
def initialize
@gensym_id = 0
end
def gensym(suffix='')
@gensym_id += 1
"g#{@gensym_id}#{suffix}"
end
def parse_template(template)
strip_whitespaces(HTree.parse(template))
end
WhiteSpacePreservingElements = {
'{http://www.w3.org/1999/xhtml}pre' => true
}
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
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
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)
out.charset = minimal_charset if out.respond_to? :charset=
out << result
out
end
def compile_template(src)
srcdoc = parse_template(src)
templates = []
body = 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
def template_attribute?(name)
/\A_/ =~ name.local_name
end
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, rest_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
ID_PAT = /[a-z][a-z0-9_]*/
NAME_FARGS_PAT = /(#{ID_PAT})(?:\(\s*(|#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)\))?/
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
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
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
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 /\A\s*'((?:[^'\\]|\\[\0-\377])*)'\s*\z/ =~ 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(/\\([\0-\377])/, '\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
def valid_syntax?(code)
begin
eval("BEGIN {return true}\n#{code.untaint}")
rescue SyntaxError
raise SyntaxError, "invalid code: #{code}"
end
end
def check_syntax(code)
unless valid_syntax?(code)
raise HTree::Error, "invalid ruby code: #{code}"
end
end
def compile_dynamic_text(ignore_tag, expr)
check_syntax(expr)
logic = [:text, expr]
logic = [:tag, logic] unless ignore_tag
logic
end
def compile_dynamic_tree(ignore_tag, expr)
check_syntax(expr)
logic = [:tree, expr]
logic = [:tag, logic] unless ignore_tag
logic
end
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
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
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
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
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
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
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
class TemplateNode
include HTree::Node
def initialize(*children)
@children = children.flatten.compact
end
attr_reader :children
def output(out, context)
@children.each {|c|
if c.respond_to? :call
c.call(out, context)
else
c.output(out, context)
end
}
end
end
class TemplateAttrName < HTree::Name
def output_attribute(text, out, context)
output(out, context)
out.output_string '="'
out.output_dynamic_attvalue(text.to_s)
out.output_string '"'
end
end
end
# :startdoc: