Skip to content

Instantly share code, notes, and snippets.

@judofyr
Created March 19, 2010 11:15
Show Gist options
  • Save judofyr/337428 to your computer and use it in GitHub Desktop.
Save judofyr/337428 to your computer and use it in GitHub Desktop.
class MustacheGenerator
def initialize(options = {})
@options = options
end
def compile(exp)
"\"#{compile!(exp)}\""
end
def compile!(exp)
case exp.first
when :multi
exp[1..-1].map { |e| compile!(e) }.join
when :static
str(exp[1])
when :mustache
send("on_#{exp[1]}", *exp[2..-1])
else
raise "Unhandled exp: #{exp.first}"
end
end
def on_section(name, content)
code = compile(content)
ev(<<-compiled)
if v = ctx[#{name.to_sym.inspect}]
if v == true
#{code}
else
v = [v] unless v.is_a?(Array) # shortcut when passed non-array
v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
end
end
compiled
end
def on_partial(name)
ev("ctx.partial(#{name.to_sym.inspect})")
end
def on_utag(name)
ev("ctx[#{name.to_sym.inspect}]")
end
def on_etag(name)
ev("CGI.escapeHTML(ctx[#{name.to_sym.inspect}].to_s)")
end
# An interpolation-friendly version of a string, for use within a
# Ruby string.
def ev(s)
"#\{#{s}}"
end
def str(s)
s.inspect[1..-2]
end
end
$:.unshift File.dirname(__FILE__)
require 'parser'
require 'gen'
class Mustache::Template
def compile(str = @source)
exp = MustacheParser.new.compile(str)
MustacheGenerator.new.compile(exp)
end
alias_method :to_s, :compile
end
## Improved Mustache parser
require 'strscan'
class MustacheParser
class SyntaxError < StandardError
def initialize(message, position)
@message = message
@lineno, @column, @line = position
@stripped_line = @line.strip
@stripped_column = @column - (@line.size - @line.lstrip.size)
end
def to_s
<<-EOF
#{@message}
Line #{@lineno}
#{@stripped_line}
#{' ' * @stripped_column}^
EOF
end
end
# After these types of tags, all whitespace will be skipped.
SKIP_WHITESPACE = %w[# /]
# These types of tags allows any content,
# the rest only allows \w+.
ANY_CONTENT = %w[! =]
attr_reader :scanner, :result
attr_writer :otag, :ctag
def initialize(options = {})
@options = {}
end
def regexp(thing)
/#{Regexp.escape(thing)}/
end
def otag
@otag ||= '{{'
end
def ctag
@ctag ||= '}}'
end
def compile(data)
# Keeps information about opened sections.
@sections = []
@result = [:multi]
@scanner = StringScanner.new(data)
until @scanner.eos?
scan_tags || scan_text
end
unless @sections.empty?
# We have parsed the whole file, but there's still opened sections.
type, pos, result = @sections.pop
error "Unclosed section #{type.inspect}", pos
end
@result
end
def scan_tags
return unless @scanner.scan(regexp(otag))
# 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*/)
content = if ANY_CONTENT.include?(type)
r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
scan_until_exclusive(r)
else
@scanner.scan(/\w*/)
end
error "Illegal content in tag" if content.empty?
case type
when '#'
block = [:multi]
@result << [:mustache, :section, content, block]
@sections << [content, position, @result]
@result = block
when '/'
section, pos, result = @sections.pop
@result = result
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]
when '{', '&'
type = "}" if type == "{"
@result << [:mustache, :utag, content]
else
@result << [:mustache, :etag, content]
end
@scanner.skip(/\s+/)
@scanner.skip(regexp(type)) if type
unless close = @scanner.scan(regexp(current_ctag))
error "Unclosed tag"
end
@scanner.skip(/\s+/) if SKIP_WHITESPACE.include?(type)
end
def scan_text
text = scan_until_exclusive(regexp(otag))
if text.nil?
# Couldn't find any otag, which means the rest is just static text.
text = @scanner.rest
# Mark as done.
@scanner.clear
end
@result << [:static, text]
end
# Scans the string until the pattern is matched. Returns the substring
# *excluding* the end of the match, advancing the scan pointer to that
# location. If there is no match, nil is returned.
def scan_until_exclusive(regexp)
pos = @scanner.pos
if @scanner.scan_until(regexp)
@scanner.pos -= @scanner.matched.size
@scanner.pre_match[pos..-1]
end
end
# Returns [lineno, column, line]
def position
# The rest of the current line
rest = @scanner.check_until(/\n|\Z/).to_s.chomp
# What we have parsed so far
parsed = @scanner.string[0...@scanner.pos]
lines = parsed.split("\n")
return lines.size, lines.last.size - 1, lines.last + rest
end
def error(message, pos = position)
raise SyntaxError.new(message, pos)
end
end
if $0 == __FILE__
require 'pp'
lexer = MustacheParser.new
pp lexer.compile(<<-EOF)
<h1>{{header}}</h1>
{{#items}}
{{#first}}
<li><strong>{{name}}</strong></li>
{{/first}}
{{#link}}
<li><a href="{{url}}">{{name}}</a></li>
{{/link}}
{{/items}}
{{#empty}}
<p>The list is empty.</p>
{{/empty}}
EOF
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment