Skip to content

Commit

Permalink
Compile into a Proc (for speed). This required more changes:
Browse files Browse the repository at this point in the history
* Introduced Template and Context
* Turned several instance methods into class methods
* Updated README, tests and sinatra.rb accordingly
  • Loading branch information
judofyr authored and defunkt committed Oct 5, 2009
1 parent 7131af1 commit 5b3b4c3
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 140 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,16 @@ Now `Simple` will look for `simple.html` in the directory it resides
in, no matter the cwd.

If you want to just change what template is used you can set
`Mustache#template_file` directly:
`Mustache.template_file` directly:

Simple.new.template_file = './blah.html'
Simple.template_file = './blah.html'

You can also go ahead and set the template directly:

Simple.template = 'Hi {{person}}!'

You can also set a different template for only a single instance:

Simple.new.template = 'Hi {{person}}!'

Whatever works.
Expand Down
302 changes: 168 additions & 134 deletions lib/mustache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,183 @@

# Blah blah blah?
# who knows.
class Mustache
# Helper method for quickly instantiating and rendering a view.
def self.to_html
new.to_html
end
class Mustache
class Template
def initialize(source, mustache)
@source = source
@mustache = mustache
@tmpid = 0
end

# The path informs your Mustache subclass where to look for its
# corresponding template.
def self.path=(path)
@path = File.expand_path(path)
end
def render(context)
(@compiled ||= compile_proc).call(context)
end

def compile(src = @source)
"\"#{compile_sections(src)}\""
end

def compile_proc(src = @source)
eval("proc{|ctx|#{compile(src)}}")
end

private

# {{#sections}}okay{{/sections}}
#
# Sections can return true, false, or an enumerable.
# If true, the section is displayed.
# If false, the section is not displayed.
# If enumerable, the return value is iterated over (a for loop).
def compile_sections(src)
res = ""
while src =~ /\{\{\#(.+)\}\}\s*(.+)\{\{\/\1\}\}\s*/m
res << compile_tags($`)
name = $1.strip.to_sym.inspect
code = compile($2)
ctxtmp = "ctx#{tmpid}"
res << ev("(v = ctx[#{name}]) ? v.respond_to?(:each) ? "\
"(#{ctxtmp}=ctx; r=v.map{|h|ctx.merge!(h);#{code}}.join;ctx=#{ctxtmp};r) : #{code} : ''")
src = $'
end
res << compile_tags(src)
end

# Find and replace all non-section tags.
# In particular we look for four types of tags:
# 1. Escaped variable tags - {{var}}
# 2. Unescaped variable tags - {{{var}}}
# 3. Comment variable tags - {{! comment}
# 4. Partial tags - {{< partial_name }}
def compile_tags(src)
res = ""
while src =~ /\{\{(!|<|\{)?([^\/#]+?)\1?\}\}+/
res << str($`)
case $1
when '!'
# ignore comments
when '<'
res << compile_partial($2.strip)
when '{'
res << utag($2.strip)
else
res << etag($2.strip)
end
src = $'
end
res << str(src)
end

# Partials are basically a way to render views from inside other views.
def compile_partial(name)
klass = Mustache.classify(name)
if Object.const_defined?(klass)
ev("#{klass}.to_html")
else
src = File.read(@mustache.path + '/' + name + '.html')
compile(src)[1..-2]
end
end

# Generate a temporary id.
def tmpid
@tmpid += 1
end

def str(s)
s.inspect[1..-2]
end

def etag(s)
ev("Mustache.escape(ctx[#{s.strip.to_sym.inspect}])")
end

def self.path
@path || '.'
def utag(s)
ev("ctx[#{s.strip.to_sym.inspect}]")
end

def ev(s)
"#\{#{s}}"
end
end

# Templates are self.class.name.underscore + '.html' -- a class of
# Dashboard would have a template (relative to the path) of
# dashboard.html
def template_file
@template_file ||= self.class.path + '/' + underscore(self.class.to_s) + '.html'
class Context < Hash
def initialize(mustache)
@mustache = mustache
super()
end

def [](name)
if has_key?(name)
super
elsif @mustache.respond_to?(name)
@mustache.send(name)
else
raise "Can't find #{name} in #{inspect}"
end
end
end

def template_file=(template_file)
@template_file = template_file
class << self
# Helper method for quickly instantiating and rendering a view.
def to_html
new.to_html
end

# The path informs your Mustache subclass where to look for its
# corresponding template.
def path=(path)
@path = File.expand_path(path)
end

def path
@path || '.'
end

# Templates are self.class.name.underscore + '.html' -- a class of
# Dashboard would have a template (relative to the path) of
# dashboard.html
def template_file
@template_file ||= path + '/' + underscore(to_s) + '.html'
end

def template_file=(template_file)
@template_file = template_file
end

def template
@template ||= templateify(File.read(template_file))
end

# template_partial => TemplatePartial
def classify(underscored)
underscored.split(/[-_]/).map { |part| part[0] = part[0].chr.upcase; part }.join
end

# TemplatePartial => template_partial
def underscore(classified)
string = classified.dup.split('::').last
string[0] = string[0].chr.downcase
string.gsub(/[A-Z]/) { |s| "_#{s.downcase}"}
end

# Escape HTML.
def escape(string)
CGI.escapeHTML(string.to_s)
end

def templateify(obj)
obj.is_a?(Template) ? obj : Template.new(obj.to_s, self)
end
end

# The template itself. You can override this if you'd like.
def template
@template ||= File.read(template_file)
@template ||= self.class.template
end

def template=(template)
@template = template
@template = self.class.templateify(template)
end

# Pass a block to `debug` with your debug putses. Set the `DEBUG`
Expand All @@ -51,7 +194,7 @@ def debug
# Kind of a hack for now, but useful when you're in an iterating section
# and want access to the hash currently being iterated over.
def context
@context ||= {}
@context ||= Context.new(self)
end

# Context accessors
Expand All @@ -70,117 +213,8 @@ def to_html

# Parses our fancy pants template HTML and returns normal HTML with
# all special {{tags}} and {{#sections}}replaced{{/sections}}.
def render(html, context = {})
# Set the context so #find and #context have access to it
@context = context = (@context || {}).merge(context)

html = render_sections(html)

# Re-set the @context because our recursion probably overwrote it
@context = context

render_tags(html)
end

# {{#sections}}okay{{/sections}}
#
# Sections can return true, false, or an enumerable.
# If true, the section is displayed.
# If false, the section is not displayed.
# If enumerable, the return value is iterated over (a for loop).
def render_sections(template)
# fail fast
return template unless template.include?('{{#')

template.gsub(/\{\{\#(.+)\}\}\s*(.+)\{\{\/\1\}\}\s*/m) do |s|
ret = find($1)

if ret.respond_to? :each
ret.map do |ctx|
render($2, ctx)
end.join
elsif ret
render($2)
else
''
end
end
end

# Find and replace all non-section tags.
# In particular we look for four types of tags:
# 1. Escaped variable tags - {{var}}
# 2. Unescaped variable tags - {{{var}}}
# 3. Comment variable tags - {{! comment}
# 4. Partial tags - {{< partial_name }}
def render_tags(template)
# fail fast
return template unless template.include?('{{')

template.gsub(/\{\{(!|<|\{)?([^\/#]+?)\1?\}\}+/) do
case $1

when '!'
# Comments are ignored
''

when '<'
# Partials are pulled in relative to `path`
partial($2)

when '{'
# The triple mustache is unescaped.
find($2)

else
# The double mustache is escaped.
escape find($2)

end
end
end

# Partials are basically a way to render views from inside other views.
def partial(name)
# First we check if a partial's view class already exists
klass = classify(name)

if Object.const_defined? klass
# If so we can cheat and render that
Object.const_get(klass).to_html
else
# If not we need to render the file directly.
render File.read(self.class.path + '/' + name + '.html'), context
end
end

# template_partial => TemplatePartial
def classify(underscored)
underscored.split(/[-_]/).map { |part| part[0] = part[0].chr.upcase; part }.join
end

# TemplatePartial => template_partial
def underscore(classified)
string = classified.dup.split('::').last
string[0] = string[0].chr.downcase
string.gsub(/[A-Z]/) { |s| "_#{s.downcase}"}
end

# Escape HTML.
def escape(string)
CGI.escapeHTML(string.to_s)
end

# Given an atom, finds a value. We'll check the current context (for both
# strings and symbols) then call methods on the view object.
def find(name)
name.strip!
if @context.has_key? name.to_sym
@context[name.to_sym]
elsif respond_to? name
send name
else
raise "Can't find #{name} in #{@context.inspect}"
end
def render(html)
html = self.class.templateify(html)
html.render(context)
end
end
2 changes: 1 addition & 1 deletion lib/mustache/sinatra.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def mustache(template, options={}, locals={})
# This is called by Sinatra's `render` with the proper paths
# and, potentially, a block containing a sub-view
def render_mustache(template, data, options, locals, &block)
name = Mustache.new.classify(template.to_s)
name = Mustache.classify(template.to_s)

if defined?(Views) && Views.const_defined?(name)
instance = Views.const_get(name).new
Expand Down
6 changes: 3 additions & 3 deletions test/mustache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ def test_unescaped
end

def test_classify
assert_equal 'TemplatePartial', Mustache.new.classify('template_partial')
assert_equal 'TemplatePartial', Mustache.classify('template_partial')
end

def test_underscore
assert_equal 'template_partial', Mustache.new.underscore('TemplatePartial')
assert_equal 'template_partial', Mustache.underscore('TemplatePartial')
end

def test_namespaced_underscore
assert_equal 'stat_stuff', Mustache.new.underscore('Views::StatStuff')
assert_equal 'stat_stuff', Mustache.underscore('Views::StatStuff')
end
end

0 comments on commit 5b3b4c3

Please sign in to comment.