module Hobo::Dryml
class TemplateEnvironment
class << self
def inherited(subclass)
subclass.compiled_local_names = []
end
attr_accessor :load_time, :compiled_local_names
def _register_tag_attrs(tag_name, attrs)
@tag_attrs ||= {}
@tag_attrs[tag_name] = attrs
end
def tag_attrs
@tag_attrs ||= {}
end
alias_method :delayed_alias_method_chain, :alias_method_chain
end
for mod in ActionView::Helpers.constants.grep(/Helper$/).map {|m| ActionView::Helpers.const_get(m)}
include mod
end
def initialize(view_name=nil, view=nil)
unless view_name.nil? && view.nil?
@view = view
@_view_name = view_name
@_erb_binding = binding
@_part_contexts = {}
@_scoped_variables = ScopedVariables.new
# Make sure the "assigns" from the controller are available (instance variables)
if view
view.assigns.each do |key, value|
instance_variable_set("@#{key}", value)
end
# copy view instance variables over
view.instance_variables.each do |iv|
instance_variable_set(iv, view.instance_variable_get(iv))
end
end
end
end
for attr in [:erb_binding, :part_contexts, :view_name,
:this, :this_parent, :this_field, :this_key,
:form_this, :form_field_names]
class_eval "def #{attr}; @_#{attr}; end"
end
def form_field_path
@_form_field_path.nil? and raise Hobo::Dryml::DrymlException,
"DRYML cannot provide the correct form-field name here (this_field = #{this_field.inspect}, this = #{this.inspect})"
@_form_field_path
end
def this_key=(key)
@_this_key = key
end
# The type of this, or when this is nil, the type that would be expected in the current field
def this_type
@_this_type ||= if this == false || this == true
Hobo::Boolean
elsif this
this.class
elsif this_parent && this_field && (parent_class = this_parent.class).respond_to?(:attr_type)
type = parent_class.attr_type(this_field)
if type.is_a?(ActiveRecord::Reflection::AssociationReflection)
reflection = type
if reflection.macro == :has_many
Array
elsif reflection.options[:polymorphic]
# All we know is that it will be some active-record type
ActiveRecord::Base
else
reflection.klass
end
else
type
end
else
# Nothing to go on at all
Object
end
end
def this_field_reflection
this.try.proxy_reflection ||
(this_parent && this_field && this_parent.class.respond_to?(:reflections) && this_parent.class.reflections[this_field.to_sym])
end
def attrs_for(name)
self.class.tag_attrs[name.to_sym]
end
def add_classes!(attributes, *classes)
classes = classes.flatten.select{|x|x}
current = attributes[:class]
attributes[:class] = (current ? current.split + classes : classes).uniq.join(' ')
attributes
end
def add_classes(attributes, *classes)
add_classes!(HashWithIndifferentAccess.new(attributes), classes)
end
def merge_attrs(attrs, overriding_attrs)
return {}.update(attrs) if overriding_attrs.nil?
attrs = attrs.with_indifferent_access unless attrs.is_a?(HashWithIndifferentAccess)
classes = overriding_attrs[:class]
attrs = add_classes(attrs, *classes.split) if classes
attrs.update(overriding_attrs - [:class])
end
def scope
@_scoped_variables
end
def dom_id(object=nil, attribute=nil)
if object.nil?
# nothing passed -- use context
if this_parent && this_field
object, attribute = this_parent, this_field
else
object = this
end
end
if (id = object.try.typed_id)
attribute ? "#{id}_#{attribute}" : id
else
"nil"
end
end
def call_part(part_node_id, part_name, part_this=nil, *locals)
res = ''
if part_this
new_object_context(part_this) do
@_part_contexts[part_node_id] = PartContext.new(part_name, dom_id, locals)
res = send("#{part_name}_part", *locals)
end
else
new_context do
@_part_contexts[part_node_id] = PartContext.new(part_name, dom_id, locals)
res = send("#{part_name}_part", *locals)
end
end
res
end
def call_polymorphic_tag(name, *args)
name = name.to_s.gsub('-', '_')
type = args.first.is_a?(Class) ? args.shift : nil
attributes, parameters = args
tag = find_polymorphic_tag(name, type)
if tag != name
send(tag, attributes || {}, parameters || {})
else
block_given? ? yield : nil
end
end
def find_polymorphic_tag(name, call_type=nil)
call_type ||= (this.respond_to?(:member_class) && this.member_class) || this_type
while true
if respond_to?(poly_name = "#{name}__for_#{call_type.name.to_s.underscore.gsub('/', '__')}")
return poly_name
else
if call_type == ActiveRecord::Base || call_type == Object
return name
else
call_type = call_type.superclass
end
end
end
end
def repeat_attribute(string_or_array)
res = nil
if string_or_array.instance_of?(String)
new_field_context(string_or_array) do
res = context_map { yield }
end
else
res = context_map(string_or_array) { yield }
end
res.join
end
def _erbout
@_erb_output
end
def _output(s)
@_erb_output.concat(s)
end
def new_context
ctx = [ @_erb_output,
@_this, @_this_parent, @_this_field, @_this_type,
@_form_field_path ]
@_erb_output = ""
@_this_type = nil
res = yield
@_erb_output, @_this, @_this_parent, @_this_field, @_this_type, @_form_field_path = ctx
res.to_s
end
def new_object_context(new_this)
new_context do
if new_this.respond_to?(:origin)
@_this_parent, @_this_field = new_this.origin, new_this.origin_attribute
else
@_this_parent, @_this_field = [nil, nil]
end
@_this = new_this
# We might have lost track of where 'this' is relative to the form_this
# check if this or this_parent are objects we've seen before in this form
@_form_field_path = find_form_field_path(new_this) if @_form_field_path
yield
end
end
def new_field_context(field_path, new_this=nil)
new_context do
path = if field_path.is_a? String
field_path.split('.')
else
Array(field_path)
end
if new_this
raise ArgumentError, "invlaid context change" unless path.length == 1
@_this_parent, @_this_field, @_this = this, path.first, new_this
else
parent, field, obj = Hobo.get_field_path(this, path)
@_this, @_this_parent, @_this_field = obj, parent, field
end
if @_form_field_path
@_form_field_path += path
@_form_field_paths_by_object[@_this] = @_form_field_path
end
yield
end
end
def find_form_field_path(object)
back = []
while object
path = @_form_field_paths_by_object[object]
if path
path = path + back unless back.empty?
return path
end
if object.respond_to? :origin
back.unshift object.origin_attribute
object = object.origin
else
return nil
end
end
end
def _tag_context(attributes)
with = attributes[:with]
field = attributes[:field]
if with && field
new_object_context(with) { new_field_context(field) { yield } }
elsif field
new_field_context(field) { yield }
elsif attributes.has_key?(:with)
new_object_context(with) { yield }
else
new_context { yield }
end
end
def with_form_context
@_form_this = this
@_form_field_path = []
@_form_field_names = []
@_form_field_paths_by_object = { @_form_this => [] }
res = yield
field_names = @_form_field_names
@_form_this = @_form_field_path = @_form_field_names = @_form_field_paths_by_object = nil
[res, field_names]
end
def register_form_field(name)
@_form_field_names << name
end
def _tag_locals(attributes, locals)
attributes.symbolize_keys!
#ensure with and field are not in attributes
attributes.delete(:with)
attributes.delete(:field)
# declared attributes don't appear in the attributes hash
stripped_attributes = HashWithIndifferentAccess.new.update(attributes)
locals.each {|a| stripped_attributes.delete(a.to_sym) }
# Return locals declared as local variables (attrs="...")
locals.map {|a| attributes[a.to_sym]} + [stripped_attributes]
end
def call_tag_parameter_with_default_content(the_tag, attributes, default_content, overriding_content_proc)
if the_tag.is_a?(String, Symbol) && the_tag.to_s.in?(Hobo.static_tags)
body = if overriding_content_proc
new_context { overriding_content_proc.call(proc { default_content._?.call(nil) }) }
elsif default_content
new_context { default_content.call(nil) }
else
nil
end
element(the_tag, attributes, body)
else
d = if overriding_content_proc
proc { |default| overriding_content_proc.call(proc { default_content._?.call(default) }) }
else
proc { |default| default_content._?.call(default) }
end
send(the_tag, attributes, { :default => d })
end
end
def call_tag_parameter(the_tag, attributes, parameters, caller_parameters, param_name)
overriding_proc = caller_parameters[param_name]
replacing_proc = caller_parameters[:"#{param_name}_replacement"]
unless param_name == the_tag || param_name == :default
classes = attributes[:class]
param_class = param_name.to_s.gsub('_', '-')
attributes[:class] = if classes
classes =~ /\b#{param_class}\b/ ? classes : "#{classes} #{param_class}"
else
param_class
end
end
if param_name == :default && overriding_proc
# :default content is handled specially
call_tag_parameter_with_default_content(the_tag, attributes, parameters[:default], overriding_proc)
elsif replacing_proc
# The caller is replacing this parameter. Don't call the tag
# at all, just the overriding proc, but pass the restorable
# tag as a parameter to the overriding proc
tag_restore = proc do |restore_attrs, restore_params|
# Call the replaced tag with the attributes and parameters
# as given in the original tag definition, and with the
# specialisation given on the 'restore' call
if overriding_proc
overriding_attributes, overriding_parameters = overriding_proc.call
restore_attrs = overriding_attributes.merge(restore_attrs)
restore_params = overriding_parameters.merge(restore_params)
end
override_and_call_tag(the_tag, attributes, parameters, restore_attrs, restore_params)
end
replacing_proc.call(tag_restore)
else
overriding_attributes, overriding_parameters = overriding_proc._?.call
override_and_call_tag(the_tag, attributes, parameters, overriding_attributes, overriding_parameters)
end
end
def override_and_call_tag(the_tag, general_attributes, general_parameters, overriding_attributes, overriding_parameters)
attributes = overriding_attributes ? merge_attrs(general_attributes, overriding_attributes) : general_attributes
if overriding_parameters
overriding_default_content = overriding_parameters.delete(:default)
parameters = general_parameters.merge(overriding_parameters)
else
parameters = general_parameters
end
default_content = parameters[:default]
if the_tag.is_a?(String, Symbol) && the_tag.to_s.in?(Hobo.static_tags)
body = if overriding_default_content
new_context { overriding_default_content.call(proc { default_content.call(nil) if default_content }) }
elsif default_content
new_context { default_content.call(nil) }
else
nil
end
element(the_tag, attributes, body)
else
if default_content || overriding_default_content
d = if overriding_default_content
proc { |default| overriding_default_content.call(proc { default_content.call(default) if default_content }) }
else
proc { |default| default_content.call(default) if default_content }
end
parameters = parameters.merge(:default => d)
end
if the_tag.is_a?(String, Symbol)
# It's a defined DRYML tag
send(the_tag, attributes, parameters)
else
# It's a proc - restoring a replaced parameter
the_tag.call(attributes, parameters)
end
end
end
# This proc is used where 'param' is declared on a tag that is
# itself a parameter tag. Takes two procs that each return a pair
# of hashes (attributes and parameters). Returns a single proc
# that also returns a pair of hashes - the merged atributes and
# parameters.
def merge_tag_parameter(general_proc, overriding_proc)
if overriding_proc.nil?
general_proc
else
if overriding_proc.arity == 1
# The override is a replace parameter - just pass it on
overriding_proc
else
proc do
overriding_attrs, overriding_parameters = overriding_proc.call
general_attrs, general_parameters = general_proc.call
attrs = merge_attrs(general_attrs, overriding_attrs)
overriding_default = overriding_parameters.delete(:default)
params = general_parameters.merge(overriding_parameters)
# The overrider should provide its :default as the new
# 'default_content'
if overriding_default
params[:default] =
if general_parameters[:default]
proc do |default|
overriding_default.call(proc { new_context { _output(general_parameters[:default].call(default)) } } )
end
else
proc do |default|
overriding_default.call(default)
end
end
end
[attrs, params]
end
end
end
end
def part_contexts_javascripts
storage = part_contexts_storage
storage.blank? ? "" : "\n"
end
def part_contexts_storage
PartContext.client_side_storage(@_part_contexts, session)
end
def render_tag(tag_name, attributes)
method_name = tag_name.to_s.gsub('-', '_')
if respond_to?(method_name)
res = send(method_name, attributes).strip
# TODO: Temporary hack to get the dryml metadata comments in the right place
if false && RAILS_ENV == "development"
res.gsub(/^(.*?)().*?()/m, "\\2\\3\\1")
else
res
end
else
false
end
end
def element(name, attributes, content=nil, escape = true, empty = false, &block)
unless attributes.blank?
attrs = []
if escape
attributes.each do |key, value|
next unless value
key = key.to_s.gsub("_", "-")
value = if ActionView::Helpers::TagHelper::BOOLEAN_ATTRIBUTES.include?(key)
key
else
# escape once
value.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
end
attrs << %(#{key}="#{value}")
end
else
attrs = options.map do |key, value|
key = key.to_s.gsub("_", "-")
%(#{key}="#{value}")
end
end
attr_string = " #{attrs.sort * ' '}" unless attrs.empty?
end
content = new_context { yield } if block_given?
res = if empty
"<#{name}#{attr_string}#{scope.xmldoctype ? ' /' : ''}>"
else
"<#{name}#{attr_string}>#{content}#{name}>"
end
if block && eval("defined? _erbout", block.binding) # in erb context
_output(res)
else
res
end
end
def session
@view ? @view.session : {}
end
def method_missing(name, *args, &b)
if @view
@view.send(name, *args, &b)
else
raise NoMethodError, name.to_s
end
end
end
end