Permalink
Browse files

Compile into a Proc (for speed). This required more changes:

* Introduced Template and Context
* Turned several instance methods into class methods
* Updated README, tests and sinatra.rb accordingly
  • Loading branch information...
1 parent 7131af1 commit 5b3b4c34000d7e7f6b655eb0f6d68d98ed41c373 @judofyr judofyr committed with defunkt Oct 5, 2009
Showing with 178 additions and 140 deletions.
  1. +6 −2 README.md
  2. +168 −134 lib/mustache.rb
  3. +1 −1 lib/mustache/sinatra.rb
  4. +3 −3 test/mustache_test.rb
View
8 README.md
@@ -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.
View
302 lib/mustache.rb
@@ -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`
@@ -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
@@ -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
View
2 lib/mustache/sinatra.rb
@@ -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
View
6 test/mustache_test.rb
@@ -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.