module Zafu
PARAM_KEY_REGEXP = %r{^\s+([\w_\-\[\]:]+)=}m
PARAM_VALUE_REGEXP = %r{('|")(|[^\1]*?[^\\])\1}m
module ParsingRules
# The context informs the rendering element about the current Node, node class, existing ids, etc. The
# context is inherited by sub-elements.
attr_reader :context
# The helper is used to connect the compiler to the world of the application (read/write templates, access traductions, etc)
attr_reader :helper
# The markup (of class Markup) holds information on the tag (
), tag attributes (.. class='foo') and
# indentation information that should be used when rendered. This context is not inherited.
attr_accessor :markup
# We need this flag to detect cases like
attr_reader :sub_do
def self.included(base)
base.before_parse :remove_erb
base.before_process :unescape_ruby
end
# This callback is run just after the block is initialized (Parser#initialize).
def start(mode)
# tag_context
@markup = Zafu::Markup.new(@options.delete(:html_tag))
# html_tag
if html_params = @options.delete(:html_tag_params)
@markup.params = html_params
end
# end_tag is used to know when to close parsing in sub-do
# Example:
#
#
#
#
@end_tag = @markup.tag || @options.delete(:end_tag) || "r:#{@method}"
@end_tag_count = 1
# code indentation
@markup.space_before = @options.delete(:space_before)
if sub = @params.delete(:do)
# we have a sub 'do'
sub_method = sub.delete(:method)
# We need this flag to detect cases cases like
@sub_do = true
opts = {:method => sub_method, :params => sub}
# the matching zafu tag will be parsed by the last 'do', we must inform it to halt properly :
opts[:end_tag] = @end_tag
sub = make(:void, opts)
@markup.space_after = sub.markup.space_after
sub.markup.space_after = ""
end
# set name used for include/replace from html_tag if not already set by superclass
@name = extract_name
if !@markup.tag && (@markup.tag = @params.delete(:tag))
# Extract html tag parameters from @params
@markup.steal_html_params_from(@params)
end
if @method == 'include' && @params[:template]
include_template
elsif mode == :tag && !sub
scan_tag
elsif !sub
enter(mode)
end
end
# Used to debug parser.
def to_s
"[#{@method}#{@name.blank? ? '' : " '#{@name}'"}#{@params.empty? ? '' : " #{@params.map{|k,v| ":#{k}=>#{v.inspect}"}.join(', ')}"}]" + (@blocks||[]).join('') + "[/#{@method}]"
end
def extract_name
@options[:name] ||
(%w{input select textarea}.include?(@method) ? nil : @params[:name]) ||
@markup.params[:id] ||
@params[:id]
end
def remove_erb(text)
text.gsub('<%', '<%').gsub('%>', '%>').gsub(/<\Z/, '<')
end
def unescape_ruby
@params.each do |k,v|
v.gsub!('>', '>')
v.gsub!('<', '<')
end
@method.gsub!('>', '>')
@method.gsub!('<', '<')
end
def single_child_method
return @single_child_method if defined?(@single_child_method)
@single_child_method = if @blocks.size == 1
single_child = @blocks[0]
return nil if single_child.kind_of?(String)
single_child.markup.tag ? nil : single_child.method
else
nil
end
end
# scan rules
def scan
#puts "SCAN(#{@method}): [#{@text[0..20]}]"
if @text =~ %r{\A([^<]*?)(\s*)//!}m
# comment
found = $1
flush found
eat $2
scan_comment
elsif @text =~ /\A(([^<]*?)(^ *|))]+)>/m
# Doctype/xml
flush $&
end
elsif $1.last == ' ' && @text[0..1] == '< '
# solitary ' < '
store space
flush '< '
scan
else
scan_tag(:space_before => space)
end
else
# no more tags
flush
end
end
def scan_close_tag
if @text =~ /\A<\/([^>]+)>( *\n+|)/m
# puts "CLOSE:[#{$&}]}" # ztag
# closing tag
if $1 == @end_tag
@end_tag_count -= 1
if @end_tag_count == 0
eat $&
@markup.space_after = $2
leave
else
# keep the tag (false alert)
flush $&
end
elsif $1[0..1] == 'r:'
# /rtag
eat $&
if $1 != @end_tag
# error bad closing rtag
store "#{$&.gsub('<', '<').gsub('>','>')} should be </#{@end_tag}>"
end
leave
else
# other html tag closing
flush $&
end
else
# error
flush
end
end
def scan_html_comment(opts={})
if @text =~ /\A/m
# zafu html escaped
#puts "ZAFU_HTML_ESCAPED[#{$&}]"
eat $&
@text = opts[:space_before] + $1 + @text
elsif @text =~ /\A/m
# html comment
#puts "HTML_COMMENT[#{$&}]"
flush $&
else
# error
flush
end
end
def scan_comment
if @text =~ %r{\A//!.*(\n|\Z)}
# zafu html escaped
eat $&
else
# error
flush
end
end
def get_params
params = Zafu::OrderedHash.new
raw = ''
while @text =~ PARAM_KEY_REGEXP
raw << $&
eat $&
key = $1
if @text =~ PARAM_VALUE_REGEXP
raw_t = $&
quote = $1
eat $&
value = $2.gsub("\\#{quote}", quote)
if key == 'do'
# Sub do
sub, raw = get_params
sub[:method] = value
params[:do] = sub
return params
else
raw << raw_t
params[key.to_sym] = value
end
end
end
return params, raw
end
def scan_tag(opts={})
#puts "TAG(#{@method}): [#{@text[0..20]}]"
# FIXME: Better parameters parsing could avoid the > hack. Create a "scan_params" method.
if @text =~ /\A/
eat $&
opts.merge!(:method=>method, :params=>params)
opts.merge!(:text=>'') if $1 != ''
make(:void, opts)
else
# ERROR
flush
end
#elsif @text =~ /\A<(\w+)([^>]*?)do\s*=('([^>]*?[^\\]|)'|"([^>]*?[^\\]|)")([^>]*?)(\/?)>/
elsif @text =~ /\A<([\w:]+)/
html_tag = $1
eat $&
params, raw = get_params
#puts "HTML(#{html_tag}):[#{@text}]" # html tag
if @text =~ /\A\s*(\/?)>/
eat $&
is_end_tag = !$1.blank?
if sub = params.delete(:do)
# puts "SUB_DO:#{params.inspect}"
# do tag
method = sub.delete(:method)
opts.merge!(:text=>'') if is_end_tag
opts.merge!(
:html_tag => html_tag,
:html_tag_params => params,
:method => method,
:params => sub
)
make(:void, opts)
elsif raw =~ /\#\{/ || params[:id]
# puts "HTML_DYN|ID:#{@params.inspect}"
# If we have an :id, we need to store this as a block in case it is replaced
# html tag with dynamic params
opts.merge!(:text=>'') if is_end_tag
opts.merge!(:method => 'void', :html_tag => html_tag, :html_tag_params => params)
make(:void, opts)
elsif @end_tag && html_tag == @end_tag
#puts "PLAIN(END):#{@params.inspect}"
# plain html tag
store "#{opts[:space_before]}<#{html_tag}#{raw}#{is_end_tag ? '/' : ''}>"
@end_tag_count += 1 unless is_end_tag
elsif %w{link img script}.include?(html_tag)
#puts "ASSET: [#{@text}]"
opts.merge!(:text=>'') if is_end_tag
opts.merge!(:method => 'rename_asset', :html_tag_params => params, :params => params, :html_tag => html_tag)
make(:asset, opts)
else
#puts "PLAIN:<#{html_tag}#{raw}#{is_end_tag ? '/' : ''}>"
# plain html tag
store "#{opts[:space_before]}<#{html_tag}#{raw}#{is_end_tag ? '/' : ''}>"
end
else
# ERROR
flush
end
else
# unknown tag type
store %Q{Invalid tag near '#{@text[0..10].gsub('>','>').gsub('<','<')}'}
@text = ''
end
end
def scan_asset
@end_tag = @markup.tag
if @markup.tag == 'script'
enter(:void)
else
enter(:inside_asset)
end
end
def scan_inside_asset
if @text =~ /\A(.*?)<\/#{@end_tag.gsub('?', '\\?')}>/m
eat $&
store $1
leave(:asset)
else
# never ending asset
flush
end
end
# Helper during compilation to make a block
def add_block(text_or_opts, at_start = false)
# avoid wrapping objects in [void][/void]
bak = @blocks
@blocks = []
if text_or_opts.kind_of?(String)
new_blocks = make(:void, :method => 'void', :text => text_or_opts).blocks
else
new_blocks = [make(:void, text_or_opts)]
end
if at_start
bak = new_blocks + bak
else
bak += new_blocks
end
@blocks = bak
# Force descendants rebuild
@all_descendants = nil
end
# Helper during compilation to wrap current content in a new block
def wrap_in_block(text_or_opts)
# avoid wrapping objects in [void][/void]
bak = @blocks
@blocks = []
if text_or_opts.kind_of?(String)
wrapper = make(:void, :method => 'void', :text => text_or_opts)
else
wrapper = make(:void, text_or_opts)
end
wrapper.blocks = bak
@blocks = [wrapper]
# Force descendants rebuild
@all_descendants = nil
end
end # ParsingRules
end # Zafu