module MasterView
class RenderLevel #contains render modes, each gen level
attr_accessor :render_modes
def initialize(render_modes = [])
@render_modes = render_modes
end
def push(render_mode)
@render_modes.push render_mode
end
end
class RenderMode #contains tags and output, each output style
attr_accessor :output, :tags, :mode_type
def initialize(output = nil, mode_type = :normal)
@output = output
@tags = []
@mode_type = mode_type
end
def tag
@tags.last
end
def directives
@tags.last.directives
end
def render_directives(method_name, context=tag.create_context)
dcs = DirectiveCallStack.new
tag_name = tag.tag_name.downcase
depth = tags.size-1
@tags.each do |tag|
case depth
when 0
dcs << tag.directives.determine_dcs(method_name)
when 1
dcs << tag.directives.determine_dcs(determine_method(:child, tag_name, method_name))
dcs << tag.directives.determine_dcs(determine_method(:child, :any, method_name))
dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name))
dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name))
else
dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name))
dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name))
end
depth -= 1
end
dcs.context = context
dcs.render
end
def determine_method(modifier, tag_name, base)
type = "#{modifier.to_s}_#{tag_name.to_s}_#{base.to_s}"
end
end
class DirectiveCallStack
attr_reader :directives_to_call
attr_accessor :context
def initialize
@directives_to_call = []
end
def <<(directive_calls)
if directive_calls.is_a? DirectiveCallStack
@directives_to_call << directive_calls.directives_to_call
else
@directives_to_call << directive_calls
end
@directives_to_call.flatten!
self
end
def render
return [] if @directives_to_call.empty?
directive_proc = @directives_to_call.shift
directive_proc.call(self)
end
end
class DirectiveSet
attr_accessor :directives
def initialize
@directives = []
end
def <<(directive)
@directives << directive
@directives.flatten!
self
end
def determine_dcs(method_name)
method_name_sym = method_name.to_sym
dcs = DirectiveCallStack.new
@directives.each do |directive|
if directive.respond_to? method_name_sym
dcs << create_call_proc(directive, method_name_sym)
end
end
dcs
end
def create_call_proc(directive, method_name_sym)
lambda do |dcs|
directive.save_directive_call_stack(dcs) if directive.respond_to? :save_directive_call_stack
directive.send(method_name_sym, dcs)
end
end
end
class SimpleRenderHandler
def description
'SimpleRenderHandler is the default renderer for nodes, it should be invoked as the last directive and will output node normally'
end
def stag(dcs)
context = dcs.context
ret = []
ret << "<#{context[:tag].tag_name.to_s}" # allow for symbol tag_name
sorted_attributes = context[:tag].attributes.sort { |a,b| a[0].to_s <=> b[0].to_s } #allow for symbols using to_s
sorted_attributes.each do |name, value|
ret << " #{name.to_s}=\"#{value}\"" # allow for key to by symbol
end
ret << '>' #must output as separate string so simplify_empty_elements can find it
end
def characters(dcs)
context = dcs.context
[] << context[:content_part]
end
def comment(dcs)
context = dcs.context
[] << ''
end
def cdata(dcs)
context = dcs.context
[] << ''
end
def etag(dcs)
context = dcs.context
[] << '' << "#{context[:tag].tag_name.to_s}>" #must output as separate string so simplify_empty_elements can find it
end
end
class Tag
attr_accessor :directives, :tag_name, :attributes, :mode_type, :stag, :content, :etag, :parent
def initialize(directives, tag_name, attributes, mode_type, parent)
@tag_name = tag_name
@attributes = attributes
@mode_type = mode_type
@directives = directives
@stag = []
@content = []
@etag = []
@parent = parent
end
# creates a tag context using tag itself and mode type, also merge in any additional
# values passed in via values hash
def create_context( values = {} )
{
:tag => self,
:mode_type => @mode_type
}.merge!(values)
end
def data
[] << @stag << @content << @etag
end
end
class MIOSerializer
def initialize(options)
raise RequiredArgumentMissingError.new("Required argument is missing, specify the MasterViewIO object in options[:output_mio_tree]") unless options[:output_mio_tree]
@options = options
@mio_tree = options[:output_mio_tree]
end
def serialize(render_mode, tag)
data_to_write = tag.data.join
@mio_tree.path(render_mode.output).write(data_to_write, @options)
end
end
=begin
# Serializer which simply serializes output to the console
class ConsoleSerializer
def serialize(render_mode, tag)
puts "outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}"
puts tag.data.join
puts ''
true
end
end
# Serializer which simply outputs each fragment to a hash with the key representing
# the path and the value the string contents.
# You may specify this serializer as an option to the parser (:serializer => HashSerializer.new(output_hash)).
# It takes an empty hash as the single constructor parameter to which the contents will be output.
class HashSerializer
def initialize( output_hash )
@output_hash = output_hash
end
def serialize(render_mode, tag)
Log.debug { "adding to hash - outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}" }
@output_hash[ render_mode.output ] = tag.data.join
true
end
end
=end
class Renderer
include DirectiveHelpers
# Set of element names that can be simplified. Used in simplify_empty_elements.
# Only simplify elements that are specified in the DTD as being empty, collapsing others can cause parsing
# or rendering problems in browsers.
# http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_XHTML-1.0-Strict
# base, meta, link, hr, br, param, img, area, input, col
XHTMLEmptyElementNameSet = %w{ base meta link hr br param img area input col }.to_set
attr_reader :directive_load_paths, :mv_ns, :keyword_expander, :default_extension, :options
attr_accessor :render_levels, :default_render_handler, :serializer, :template_pathname, :template_full_pathname
attr_reader :directives_registry #:nodoc:
def self.last_renderer; @@last_renderer; end
def initialize( options = {} )
@@last_renderer = self; #save last renderer for convenient access
@options = options
@default_render_handler = SimpleRenderHandler.new
@render_levels = [
RenderLevel.new( [RenderMode.new] )
]
serializer = options[:serializer] || DefaultSerializer
self.serializer = serializer.is_a?(Class) ? serializer.new(options) : serializer #one can pass in Serializer class or an instance
self.mv_ns = options[:namespace] || NamespacePrefix
self.template_pathname = options[:template_pathname]
self.template_full_pathname = IOMgr.template.path(self.template_pathname).full_pathname if self.template_pathname
@default_extension = (options[:output_mio_tree]) ? options[:output_mio_tree].default_extension : IOMgr.erb.default_extension
@keyword_expander = KeywordExpander.new
@keyword_expander.set_template_pathname(self.template_pathname, @default_extension)
#ISSUE: if :additional_directive_paths then do cleaning and validity checks here and now
# (then we don't need to keep re-checking in DirectiveRegistry or other processing)
# [DJL 04-Jul-2006]
self.directive_load_paths = ( DefaultDirectiveLoadPaths << options[:additional_directive_paths] ).flatten
end
def mv_ns=(namespace_prefix)
@mv_ns = namespace_prefix
Log.debug { 'namespace_prefix set to '+namespace_prefix }
end
# Sets directive_load_paths, re-requiring all the new load paths, however any directives that were
# already required (and loaded) will still be in memory because these are not reset.
def directive_load_paths=( directive_paths )
@directives_registry = MasterView::DirectivesRegistry
#ISSUE: can we optimize this if there aren't :additional_directive_paths?
# Do we even need/want the notion of per-invocation :additional_directive_paths
# given that the directives load path is configurable for the client app?
# [DJL 04-Jul-2006]
directives_registry.load_directives( directive_paths ) #?? if directive_load_paths != DefaultDirectiveLoadPaths ??
# is this a good idea? no clear that we need/want to modify mv_ns after app initialization
# If so, this could be simplifed. Discuss with jeffb.
# [DJL 04-Jul-2006]
if @mv_ns != NamespacePrefix
@directives_registry = directives_registry.clone
end
# only need to do the following if :additional_directive_paths or nonstd @mv_ns???
directives_registry.build_directive_maps( @mv_ns )
end
def modes
@render_levels.last.render_modes
end
def push_level(render_level)
@render_levels.push render_level
end
def push_tag(tag_name, attributes)
modes.each do |mode|
attributes_copy = attributes.clone #these get changed in select_active_directives
directives = select_active_directives(tag_name, attributes_copy, mode)
parent = (mode.tags.empty?) ? nil : mode.tag
mode.tags.push Tag.new(directives, tag_name, attributes_copy, mode.mode_type, parent)
mode.tag.stag << mode.render_directives(:stag)
end
end
def append_content(type, content)
modes.each do |mode|
if mode.tag
mode.tag.content << mode.render_directives( type, mode.tag.create_context(:content_part => content) )
end
end
end
#does not call any directives, direct output
def append_raw(raw_output)
modes.each do |mode|
if mode.tag
mode.tag.content << raw_output
end
end
end
def pop_level
@render_levels.pop
end
def pop_tag
need_to_pop_level = false
modes.each do |mode|
mode.tag.etag << mode.render_directives(:etag)
content = []
content << mode.tag.stag << mode.tag.content << mode.tag.etag
content = simplify_empty_elements content
last_tag = mode.tags.pop
if mode.tags.empty?
unless mode.output.nil?
@serializer.serialize(mode, last_tag)
need_to_pop_level = true
end
else #add it to the parent
mode.tag.content << content
end
end
pop_level if need_to_pop_level
end
# Simplify (collapse) empty elements so that from rexml parsing ends up being .
# Only simplify elements that are specified in the DTD as being empty, collapsing others can cause parsing
# or rendering problems in browsers. Uses constant XHTMLEmptyElementNameSet to find elements that should be
# collapsed.
# http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_XHTML-1.0-Strict
# xhtml-1.0-Strict empty elements are:
# base, meta, link, hr, br, param, img, area, input, col
def simplify_empty_elements(content) #relies on the fact that > and are individual strings and are back to back with nothing in between
ret = []
current_element_name = nil
next_to_last = nil
last = nil
content.flatten!
content.each do |item|
if next_to_last == '>' && last == '' && XHTMLEmptyElementNameSet.include?(current_element_name)
ret.pop #remove '>'
ret.pop #remove ''
ret << ' />' # adding in a space to make xhtml more compatible with html editors and older browsers
else
ret << item
end
unless item.nil?
if !item.starts_with?('') && item.starts_with?('<')
current_element_name = /^<(\S*)/.match(item)[1] # depending on what is after < this might be empty string
elsif last && last.starts_with?('')
current_element_name = nil
end
end
next_to_last = last
last = item
end
ret
end
def capitalize_first_letter(string)
string[0,1].upcase + string[1..-1]
end
def select_active_directives(tag_name, attributes, mode)
selected = DirectiveSet.new
directive_processors = directives_registry.construct_directive_processors( attributes )
sorted_directives = directive_processors.sort do |x,y|
xval = (x.respond_to?(:priority)) ? x.priority : DirectivePriorities::Medium
yval = (y.respond_to?(:priority)) ? y.priority : DirectivePriorities::Medium
xval <=> yval
end
sorted_directives << @default_render_handler #insure this is last
selected << sorted_directives
end
end
class MasterViewListener
include REXML::SAX2Listener
include DirectiveHelpers
def initialize( options = {} )
@renderer = Renderer.new(options)
end
def xmldecl(version, encoding, standalone)
#todo
end
def start_document
#todo
end
def doctype(name, pub, sys, long_name, uri)
#todo
end
def start_element(uri, localname, qname, attributes)
unescape_attributes!(attributes)
push_levels(attributes)
@renderer.push_tag(qname, attributes)
end
def characters(text)
@renderer.append_content(:characters, text)
end
def comment(comment)
@renderer.append_content(:comment, comment)
end
def cdata(content)
@renderer.append_content(:cdata, content)
end
def end_element(uri, localname, qname)
@renderer.pop_tag
end
def end_document
#todo
end
def unescape_attributes!(attributes)
attributes.each do |name, value|
value.replace CGI::unescapeHTML(value)
value.gsub!(''', '\'')
end
end
def generate_replace(value)
@renderer.append_raw ERB_EVAL+value+ERB_END
end
# handle a mv:gen_partial attribute, which calls generate and outputs a token
# it takes an optional :dir => 'foo/bar' which is prepended to partial path,
# otherwise it just uses what is in partial.
# This creates a generate attribute value which will be used later.
# Parameters
# value = attribute value for gen_partial
# attributes = all remaining attributes hash
def handle_gen_partial(attributes)
value = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'gen_partial')
if value
prepend_dir = find_string_val_in_string_hash(value, :dir) #only used for masterview
partial = find_string_val_in_string_hash(value, :partial)
return if partial.nil?
path = render_partial_name_to_file_name(partial, @renderer.default_extension)
path = File.join(prepend_dir, path) if prepend_dir
generate_attribute = attributes[@renderer.mv_ns+'generate'] || '' # check if we need to add to existing generate
generate_attribute = path + (generate_attribute.blank? ? '' : ', '+generate_attribute)
attributes[@renderer.mv_ns+'generate'] = generate_attribute
@renderer.append_raw( ERB_EVAL+'render( '+value+' )'+ERB_END )
end
end
def push_levels(attributes)
handle_gen_partial(attributes)
gen_replace = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'gen_replace') #get and delete from map
generate_replace( gen_replace ) unless gen_replace.nil?
gen = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'generate') #get and delete from map
if gen
omit_comment = @renderer.options[:omit_comment] || OmitGeneratedComments || File.extname(gen) != MasterView::IOMgr.erb.default_extension
attributes[@renderer.mv_ns+'insert_generated_comment'] = @renderer.template_full_pathname.to_s unless omit_comment #add the comment directive, so it will be written to each gen'd file
render_level = nil
gen_values = parse_eval_into_hash(gen, :normal)
#Log.debug { 'generate_hash='+gen_values.inspect }
gen_values.each do |key,value|
mode_type = key.to_sym
arr_values = (value.is_a?(Enumerable)) ? value : [value] #if not enumerable add it to array
value.each do |path|
path.strip!
#Log.debug { ('pushing mode='+mode_type.to_s+' path='+path).indent(2*@renderer.render_levels.size) }
render_level ||= RenderLevel.new
render_level.push RenderMode.new(path, mode_type)
end
end
@renderer.push_level(render_level) unless render_level.nil?
end
end
end
class Parser
def self.parse_mio( template_mio, output_mio_tree, options = {})
options[:template_pathname]= template_mio.pathname
options[:output_mio_tree] = output_mio_tree
template = template_mio.read
self.parse( template, options )
end
# parse a MasterView template and render output.
# template param is actual template source passed in as string or array.
# options are the optional parameters which control the output (:output_mio_tree, :namespace, :serializer)
def self.parse( template, options = DefaultParserOptions.clone)
options[:listeners] ||= [MasterViewListener]
options[:rescue_exceptions] = RescueExceptions unless options.include?(:rescue_exceptions)
begin
if options[:tidy]
template = TidyHelper.tidy(template)
elsif options[:escape_erb]
template = EscapeErbHelper.escape_erb(template)
end
parser = REXML::Parsers::SAX2Parser.new( template )
options[:listeners].each do |listener|
if listener.is_a? Class
parser.listen( listener.new(options) )
else
parser.listen(listener)
end
end
parser.parse
rescue Exception => e
if options[:rescue_exceptions]
Log.error { "Failure to parse template. Exception="+e }
Log.debug { e.backtrace.join("\n") }
else
raise
end
end
end
end
end