Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created December 10, 2011 22:58
Show Gist options
  • Save mcmire/1456900 to your computer and use it in GitHub Desktop.
Save mcmire/1456900 to your computer and use it in GitHub Desktop.
Add unnamed arguments to Mustache helpers
require 'mustache'
class Mustache
class Parser
# Override so that after we parse the initial name, we continue by checking for arguments.
#
# This allows you to not only write:
#
# {{foo}}
#
# but also:
#
# {{foo bar}}
# {{foo bar "baz quux"}}
# {{foo 'bar baz'}}
# {{foo 'bar baz', quux, blargh}}
#
# This also works with the {{{...}}} form.
#
# Compare with: https://github.com/defunkt/mustache/blob/master/lib/mustache/parser.rb#L107
#
def scan_tags
# Scan until we hit an opening delimiter.
start_of_line = @scanner.beginning_of_line?
pre_match_position = @scanner.pos
last_index = @result.length
return unless x = @scanner.scan(/([ \t]*)?#{Regexp.escape(otag)}/)
padding = @scanner[1] || ''
# Don't touch the preceding whitespace unless we're matching the start
# of a new line.
unless start_of_line
@result << [:static, padding] unless padding.empty?
pre_match_position += padding.length
padding = ''
end
# Since {{= rewrites ctag, we store the ctag which should be used
# when parsing this specific tag.
current_ctag = self.ctag
type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
@scanner.skip(/\s*/)
# ANY_CONTENT tags allow any character inside of them, while
# other tags (such as variables) are more strict.
if ANY_CONTENT.include?(type)
r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
content = scan_until_exclusive(r)
else
content = @scanner.scan(ALLOWED_CONTENT)
end
# We found {{ but we can't figure out what's going on inside.
error "Illegal content in tag" if content.empty?
fetch = [:mustache, :fetch, content.split('.')]
prev = @result
# Based on the sigil, do what needs to be done.
case type
when '#'
block = [:multi]
@result << [:mustache, :section, fetch, block]
@sections << [content, position, @result]
@result = block
when '^'
block = [:multi]
@result << [:mustache, :inverted_section, fetch, block]
@sections << [content, position, @result]
@result = block
when '/'
section, pos, result = @sections.pop
raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
(@result = result).last << raw << [self.otag, self.ctag]
if section.nil?
error "Closing unopened #{content.inspect}"
elsif section != content
error "Unclosed section #{section.inspect}", pos
end
when '!'
# ignore comments
when '='
self.otag, self.ctag = content.split(' ', 2)
when '>', '<'
@result << [:mustache, :partial, content, padding]
when '{', '&'
# The closing } in unescaped tags is just a hack for
# aesthetics.
type = "}" if type == "{"
token_type = :utag
else
token_type = :etag
end
# Skip whitespace after the content inside this tag.
@scanner.skip(/\s+/)
if token_type == :utag || token_type == :etag
tag = [fetch]
i = 0
loop do
if i > 30
# This should never happen, but it's worth checking anyway.
error "Unclosed tag"
end
# Skip any balancing sigils after the content inside this tag.
@scanner.skip(regexp(type)) if type
# Try to find the closing tag.
re = regexp(current_ctag)
if close = @scanner.scan(re)
# We're good, go on.
# A unary call {{foo}} will be parsed as [:mustache, :etag, "foo"]
# A call with args {{foo bar baz}} will be parsed as [:mustache, :etag, "foo", "bar", "baz"]
result = ([:mustache, token_type] + tag)
@result << result
break
end
# Check for arguments.
if @scanner.scan(/\s*(?:,?\s*)?(?:"([^"]+)"|'([^']+)')\s*/)
tag << (@scanner[2] || @scanner[1])
elsif @scanner.scan(/\s*(?:,?\s*)?([^ #{current_ctag}]+)\s*/)
tag << @scanner[1]
else
error "Unclosed tag"
end
i += 1
end
else
# Skip any balancing sigils after the content inside this tag.
@scanner.skip(regexp(type)) if type
# Try to find the closing tag.
re = regexp(current_ctag)
unless close = @scanner.scan(re)
error "Unclosed tag"
end
end
# If this tag was the only non-whitespace content on this line, strip
# the remaining whitespace. If not, but we've been hanging on to padding
# from the beginning of the line, re-insert the padding as static text.
if start_of_line && !@scanner.eos?
if @scanner.peek(2) =~ /\r?\n/ && SKIP_WHITESPACE.include?(type)
@scanner.skip(/\r?\n/)
else
prev.insert(last_index, [:static, padding]) unless padding.empty?
end
end
# Store off the current scanner position now that we've closed the tag
# and consumed any irrelevant whitespace.
@sections.last[1] << @scanner.pos unless @sections.empty?
return unless @result == [:multi]
end
end
class Generator
# Override to accept args and pass them onto the helper method that will be
# called.
#
# For example:
#
# on_utag([:mustache, :fetch, "foo"])
# => render(foo())
# on_utag([:mustache, :fetch, "foo"], "bar", "baz quux")
# => render(foo("bar, "baz quux"))
#
# Compare with: https://github.com/defunkt/mustache/blob/master/lib/mustache/generator.rb#L147
#
def on_utag(name, *args)
args_as_str = args.map(&:inspect).join(", ")
ev(<<-compiled)
v = #{compile!(name)}
if v.is_a?(Proc)
v = Mustache::Template.new(v.call(#{args_as_str}).to_s).render(ctx.dup)
end
v.to_s
compiled
end
# Override to accept args and pass them onto the helper method that will be
# called.
#
# For example:
#
# on_etag([:mustache, :fetch, "foo"])
# => render(foo())
# on_etag([:mustache, :fetch, "foo"], "bar", "baz quux")
# => render(foo("bar, "baz quux"))
#
# Compare with: https://github.com/defunkt/mustache/blob/master/lib/mustache/generator.rb#L158
#
def on_etag(name, *args)
args_as_str = args.map(&:inspect).join(", ")
ev(<<-compiled)
v = #{compile!(name)}
if v.is_a?(Proc)
v = Mustache::Template.new(v.call(#{args_as_str}).to_s).render(ctx.dup)
end
ctx.escapeHTML(v.to_s)
compiled
end
end
class Context
# Override so that if we are looking for a method on the current object and
# we are checking to see if we should call that method immediately, we
# take into consideration that the method could take multiple arguments, not
# just one.
#
# Compare with: https://github.com/defunkt/mustache/blob/master/lib/mustache/context.rb#L123
#
def find(obj, key, default = nil)
hash = obj.respond_to?(:has_key?)
if hash && obj.has_key?(key)
obj[key]
elsif hash && obj.has_key?(key.to_s)
obj[key.to_s]
elsif !hash && obj.respond_to?(key)
meth = obj.method(key) rescue proc { obj.send(key) }
if meth.arity == 0
meth[]
else
meth.to_proc
end
else
default
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment